From b42ade471f1015dddcf5cf8b422f717db9c953df Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 1 Nov 2016 09:03:57 +0000 Subject: [PATCH 001/228] First version http server to emulate keepasshttp plugin, for interworking with browser plugins. --- http/pom.xml | 59 ++ .../pwdb/keepasshttp/Handlers.java | 792 ++++++++++++++++++ .../linguafranca/pwdb/keepasshttp/Http.java | 446 ++++++++++ .../keepasshttp/KeePassJavaHttpHandler.java | 71 ++ .../keepasshttp/KeePassJavaJettyServer.java | 23 + .../pwdb/keepasshttp/Message.java | 179 ++++ .../pwdb/keepasshttp/Processor.java | 360 ++++++++ .../keepasshttp/util/LogginInputStream.java | 72 ++ .../keepasshttp/util/LogginOutputStream.java | 46 + .../pwdb/keepasshttp/util/LogginProxy.java | 111 +++ http/src/main/resources/logback.xml | 14 + http/src/test/java/MonitoredRequestsTest.java | 76 ++ http/src/test/java/TestRequestResponse.java | 68 ++ http/src/test/java/trace.txt | 228 +++++ .../org/linguafranca/pwdb/kdbx/Helpers.java | 13 +- pom.xml | 1 + 16 files changed, 2555 insertions(+), 4 deletions(-) create mode 100644 http/pom.xml create mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/Handlers.java create mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/Http.java create mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java create mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaJettyServer.java create mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java create mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java create mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginInputStream.java create mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginOutputStream.java create mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginProxy.java create mode 100644 http/src/main/resources/logback.xml create mode 100644 http/src/test/java/MonitoredRequestsTest.java create mode 100644 http/src/test/java/TestRequestResponse.java create mode 100644 http/src/test/java/trace.txt diff --git a/http/pom.xml b/http/pom.xml new file mode 100644 index 00000000..96f36170 --- /dev/null +++ b/http/pom.xml @@ -0,0 +1,59 @@ + + + + KeePassJava2-parent + org.linguafranca.pwdb + 2.1.1-SNAPSHOT + + 4.0.0 + + KeePassJava2-http + KeePassJava2 :: HTTP + + + com.madgag.spongycastle + core + 1.54.0.0 + + + org.linguafranca.pwdb + KeePassJava2-kdbx + 2.1.1-SNAPSHOT + + + org.eclipse.jetty + jetty-server + 9.2.19.v20160908 + + + org.slf4j + slf4j-api + ${slf4j-api.version} + + + ch.qos.logback + logback-core + ${logback.version} + + + ch.qos.logback + logback-classic + ${logback.version} + + + com.google.code.gson + gson + 2.7 + + + junit + junit + 4.12 + test + + + + + \ No newline at end of file diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Handlers.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Handlers.java new file mode 100644 index 00000000..343a7d48 --- /dev/null +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Handlers.java @@ -0,0 +1,792 @@ +package org.linguafranca.pwdb.keepasshttp; + +/** + * @author jo + */ +public class Handlers { +/* + public sealed partial class KeePassHttpExt : Plugin + { + private string GetHost(string uri) + { + var host = uri; + try + { + var url = new Uri(uri); + host = url.Host; + + if (!url.IsDefaultPort) + { + host += ":" + url.Port.ToString(); + } + } + catch + { + // ignore exception, not a URI, assume input is host + } + return host; + } + + private string GetScheme(string uri) + { + var scheme = ""; + try + { + var url = new Uri(uri); + scheme = url.Scheme; + } + catch + { + // ignore exception, not a URI, assume input is host + } + return scheme; + } + + private bool canShowBalloonTips() + { + // tray icon is not visible --> no balloon tips for it + if (Program.Config.UI.TrayIcon.ShowOnlyIfTrayed && !host.MainWindow.IsTrayed()) + { + return false; + } + + // only use balloon tips on windows machines + if (Environment.OSVersion.Platform == PlatformID.Win32NT || Environment.OSVersion.Platform == System.PlatformID.Win32S || Environment.OSVersion.Platform == System.PlatformID.Win32Windows) + { + int enabledBalloonTipsMachine = (int)Registry.GetValue("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced", + "EnableBalloonTips", + 1); + int enabledBalloonTipsUser = (int)Registry.GetValue("HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced", + "EnableBalloonTips", + 1); + return (enabledBalloonTipsMachine == 1 && enabledBalloonTipsUser == 1); + } + + return false; + } + + 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 + + root.SearchEntries(parms, list); + foreach (var entry in list) + { + var name = entry.Strings.ReadSafe(PwDefs.TitleField); + var login = GetUserPass(entry)[0]; + var uuid = entry.Uuid.ToHexString(); + var e = new ResponseEntry(name, login, null, uuid, null); + resp.Entries.Add(e); + } + 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); + } + } + + private IEnumerable FindMatchingEntries(Request r, Aes aes) + { + string submitHost = null; + string realm = null; + var listResult = new List(); + var url = CryptoTransform(r.Url, true, false, aes, CMode.DECRYPT); + string formHost, searchHost; + formHost = searchHost = GetHost(url); + string hostScheme = GetScheme(url); + if (r.SubmitUrl != null) { + submitHost = GetHost(CryptoTransform(r.SubmitUrl, true, false, aes, CMode.DECRYPT)); + } + if (r.Realm != null) + realm = CryptoTransform(r.Realm, true, false, aes, CMode.DECRYPT); + + var origSearchHost = searchHost; + var parms = MakeSearchParameters(); + + 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); + } + + 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 = 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; + } + listCount = listResult.Count; + } + + + 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) + { + if (c.Allow.Contains(formHost) && (submitHost == null || c.Allow.Contains(submitHost))) + return true; + if (c.Deny.Contains(formHost) || (submitHost != null && c.Deny.Contains(submitHost))) + return false; + if (realm != null && c.Realm != realm) + return false; + } + + if (entryUrl != null && (entryUrl.StartsWith("http://") || entryUrl.StartsWith("https://") || title.StartsWith("ftp://") || title.StartsWith("sftp://"))) + { + var uHost = GetHost(entryUrl); + if (formHost.EndsWith(uHost)) + return true; + } + + if (title.StartsWith("http://") || title.StartsWith("https://") || title.StartsWith("ftp://") || title.StartsWith("sftp://")) + { + var uHost = GetHost(title); + if (formHost.EndsWith(uHost)) + return true; + } + return formHost.Contains(title) || (entryUrl != null && formHost.Contains(entryUrl)); + }; + + Func filterSchemes = delegate(PwEntry e) + { + var title = e.Strings.ReadSafe(PwDefs.TitleField); + var entryUrl = e.Strings.ReadSafe(PwDefs.UrlField); + + if (entryUrl != null) + { + var entryScheme = GetScheme(entryUrl); + if (entryScheme == hostScheme) + { + return true; + } + } + + var titleScheme = GetScheme(title); + if (titleScheme == hostScheme) + { + return true; + } + + return false; + }; + + var result = from e in listResult where filter(e.entry) select e; + + if (configOpt.MatchSchemes) + { + result = from e in result where filterSchemes(e.entry) select e; + } + + return result; + } + + private void GetLoginsCountHandler(Request r, Response resp, Aes aes) + { + if (!VerifyRequest(r, aes)) + return; + + resp.Success = true; + resp.Id = r.Id; + var items = FindMatchingEntries(r, aes); + SetResponseVerifier(resp, aes); + resp.Count = items.ToList().Count; + } + + private void GetLoginsHandler(Request r, Response resp, Aes aes) + { + if (!VerifyRequest(r, aes)) + return; + + string submithost = null; + var host = GetHost(CryptoTransform(r.Url, true, false, aes, CMode.DECRYPT)); + if (r.SubmitUrl != null) + submithost = GetHost(CryptoTransform(r.SubmitUrl, true, false, aes, CMode.DECRYPT)); + + var items = FindMatchingEntries(r, aes); + if (items.ToList().Count > 0) + { + Func filter = delegate(PwEntry e) + { + var c = GetEntryConfig(e); + + var title = e.Strings.ReadSafe(PwDefs.TitleField); + var entryUrl = e.Strings.ReadSafe(PwDefs.UrlField); + if (c != null) + { + return title != host && entryUrl != host && !c.Allow.Contains(host) || (submithost != null && !c.Allow.Contains(submithost) && submithost != title && submithost != entryUrl); + } + return title != host && entryUrl != host || (submithost != null && title != submithost && entryUrl != submithost); + }; + + var configOpt = new ConfigOpt(this.host.CustomConfig); + var config = GetConfigEntry(true); + var autoAllowS = config.Strings.ReadSafe("Auto Allow"); + var autoAllow = autoAllowS != null && autoAllowS.Trim() != ""; + autoAllow = autoAllow || configOpt.AlwaysAllowAccess; + var needPrompting = from e in items where filter(e.entry) select e; + + if (needPrompting.ToList().Count > 0 && !autoAllow) + { + var win = this.host.MainWindow; + + using (var f = new AccessControlForm()) + { + win.Invoke((MethodInvoker)delegate + { + f.Icon = win.Icon; + f.Plugin = this; + f.Entries = (from e in items where filter(e.entry) select e.entry).ToList(); + //f.Entries = needPrompting.ToList(); + f.Host = submithost != null ? submithost : host; + f.Load += delegate { f.Activate(); }; + f.ShowDialog(win); + if (f.Remember && (f.Allowed || f.Denied)) + { + foreach (var e in needPrompting) + { + var c = GetEntryConfig(e.entry); + if (c == null) + c = new KeePassHttpEntryConfig(); + var set = f.Allowed ? c.Allow : c.Deny; + set.Add(host); + if (submithost != null && submithost != host) + set.Add(submithost); + SetEntryConfig(e.entry, c); + + } + } + if (!f.Allowed) + { + items = items.Except(needPrompting); + } + }); + } + } + + string compareToUrl = null; + if (r.SubmitUrl != null) + { + compareToUrl = CryptoTransform(r.SubmitUrl, true, false, aes, CMode.DECRYPT); + } + if(String.IsNullOrEmpty(compareToUrl)) + compareToUrl = CryptoTransform(r.Url, true, false, aes, CMode.DECRYPT); + + compareToUrl = compareToUrl.ToLower(); + + foreach (var entryDatabase in items) + { + string entryUrl = String.Copy(entryDatabase.entry.Strings.ReadSafe(PwDefs.UrlField)); + if (String.IsNullOrEmpty(entryUrl)) + entryUrl = entryDatabase.entry.Strings.ReadSafe(PwDefs.TitleField); + + entryUrl = entryUrl.ToLower(); + + entryDatabase.entry.UsageCount = (ulong)LevenshteinDistance(compareToUrl, entryUrl); + + } + + var itemsList = items.ToList(); + + if (configOpt.SpecificMatchingOnly) + { + itemsList = (from e in itemsList + orderby e.entry.UsageCount ascending + select e).ToList(); + + ulong lowestDistance = itemsList.Count > 0 ? + itemsList[0].entry.UsageCount : + 0; + + itemsList = (from e in itemsList + where e.entry.UsageCount == lowestDistance + orderby e.entry.UsageCount + select e).ToList(); + + } + + if (configOpt.SortResultByUsername) + { + var items2 = from e in itemsList orderby e.entry.UsageCount ascending, GetUserPass(e)[0] ascending select e; + itemsList = items2.ToList(); + } + else + { + var items2 = from e in itemsList orderby e.entry.UsageCount ascending, e.entry.Strings.ReadSafe(PwDefs.TitleField) ascending select e; + 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; + } + else + { + resp.Success = true; + resp.Id = r.Id; + SetResponseVerifier(resp, aes); + } + } + //http://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#C.23 + private int LevenshteinDistance(string source, string target) + { + if (String.IsNullOrEmpty(source)) + { + if (String.IsNullOrEmpty(target)) return 0; + return target.Length; + } + if (String.IsNullOrEmpty(target)) return source.Length; + + if (source.Length > target.Length) + { + var temp = target; + target = source; + source = temp; + } + + var m = target.Length; + var n = source.Length; + var distance = new int[2, m + 1]; + // Initialize the distance 'matrix' + for (var j = 1; j <= m; j++) distance[0, j] = j; + + var currentRow = 0; + for (var i = 1; i <= n; ++i) + { + currentRow = i & 1; + distance[currentRow, 0] = i; + var previousRow = currentRow ^ 1; + for (var j = 1; j <= m; j++) + { + var cost = (target[j - 1] == source[i - 1] ? 0 : 1); + distance[currentRow, j] = Math.Min(Math.Min( + distance[previousRow, j] + 1, + distance[currentRow, j - 1] + 1), + distance[previousRow, j - 1] + cost); + } + } + return distance[currentRow, m]; + } + + private ResponseEntry PrepareElementForResponseEntries(ConfigOpt configOpt, PwEntryDatabase entryDatabase) + { + var name = entryDatabase.entry.Strings.ReadSafe(PwDefs.TitleField); + var loginpass = GetUserPass(entryDatabase); + var login = loginpass[0]; + var passwd = loginpass[1]; + var uuid = entryDatabase.entry.Uuid.ToHexString(); + + List fields = null; + if (configOpt.ReturnStringFields) + { + fields = new List(); + foreach (var sf in entryDatabase.entry.Strings) + { + var sfValue = entryDatabase.entry.Strings.ReadSafe(sf.Key); + if (configOpt.ReturnStringFieldsWithKphOnly) + { + if (sf.Key.StartsWith("KPH: ")) + { + fields.Add(new ResponseStringField(sf.Key.Substring(5), sfValue)); + } + } + else + { + fields.Add(new ResponseStringField(sf.Key, sfValue)); + } + } + + if (fields.Count > 0) + { + var fields2 = from e2 in fields orderby e2.Key ascending select e2; + fields = fields2.ToList(); + } + else + { + fields = null; + } + } + + return new ResponseEntry(name, login, passwd, uuid, fields); + } + + private void SetLoginHandler(Request r, Response resp, Aes aes) + { + if (!VerifyRequest(r, aes)) + return; + + string url = CryptoTransform(r.Url, true, false, aes, CMode.DECRYPT); + var urlHost = GetHost(url); + + PwUuid uuid = null; + string username, password; + + username = CryptoTransform(r.Login, true, false, aes, CMode.DECRYPT); + password = CryptoTransform(r.Password, true, false, aes, CMode.DECRYPT); + + if (r.Uuid != null) + { + uuid = new PwUuid(MemUtil.HexStringToByteArray( + CryptoTransform(r.Uuid, true, false, aes, CMode.DECRYPT))); + } + + if (uuid != null) + { + // modify existing entry + UpdateEntry(uuid, username, password, urlHost, r.Id); + } + else + { + // create new entry + CreateEntry(username, password, urlHost, url, r, aes); + } + + resp.Success = true; + resp.Id = r.Id; + SetResponseVerifier(resp, aes); + } + + private void AssociateHandler(Request r, Response resp, Aes aes) + { + if (!TestRequestVerifier(r, aes, r.Key)) + return; + + // key is good, prompt user to save + using (var f = new ConfirmAssociationForm()) + { + var win = host.MainWindow; + win.Invoke((MethodInvoker)delegate + { + f.Activate(); + f.Icon = win.Icon; + f.Key = r.Key; + f.Load += delegate { f.Activate(); }; + f.ShowDialog(win); + + if (f.KeyId != null) + { + var entry = GetConfigEntry(true); + + bool keyNameExists = true; + while (keyNameExists) + { + DialogResult keyExistsResult = DialogResult.Yes; + foreach (var s in entry.Strings) + { + if (s.Key == ASSOCIATE_KEY_PREFIX + f.KeyId) + { + keyExistsResult = MessageBox.Show( + win, + "A shared encryption-key with the name \"" + f.KeyId + "\" already exists.\nDo you want to overwrite it?", + "Overwrite existing key?", + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning, + MessageBoxDefaultButton.Button1 + ); + break; + } + } + + if (keyExistsResult == DialogResult.No) + { + f.ShowDialog(win); + } + else + { + keyNameExists = false; + } + } + + if (f.KeyId != null) + { + entry.Strings.Set(ASSOCIATE_KEY_PREFIX + f.KeyId, new ProtectedString(true, r.Key)); + entry.Touch(true); + resp.Id = f.KeyId; + resp.Success = true; + SetResponseVerifier(resp, aes); + UpdateUI(null); + } + } + }); + } + } + + private void TestAssociateHandler(Request r, Response resp, Aes aes) + { + if (!VerifyRequest(r, aes)) + return; + + resp.Success = true; + resp.Id = r.Id; + SetResponseVerifier(resp, aes); + } + + private void GeneratePassword(Request r, Response resp, Aes aes) + { + if (!VerifyRequest(r, aes)) + return; + + byte[] pbEntropy = null; + ProtectedString psNew; + PwProfile autoProfile = Program.Config.PasswordGenerator.AutoGeneratedPasswordsProfile; + PwGenerator.Generate(out psNew, autoProfile, pbEntropy, Program.PwGeneratorPool); + + byte[] pbNew = psNew.ReadUtf8(); + if (pbNew != null) + { + uint uBits = QualityEstimation.EstimatePasswordBits(pbNew); + ResponseEntry item = new ResponseEntry(Request.GENERATE_PASSWORD, uBits.ToString(), StrUtil.Utf8.GetString(pbNew), Request.GENERATE_PASSWORD, null); + resp.Entries.Add(item); + resp.Success = true; + resp.Count = 1; + MemUtil.ZeroByteArray(pbNew); + } + + 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); + } + } + + private KeePassHttpEntryConfig GetEntryConfig(PwEntry e) + { + var serializer = NewJsonSerializer(); + if (e.Strings.Exists(KEEPASSHTTP_NAME)) + { + var json = e.Strings.ReadSafe(KEEPASSHTTP_NAME); + using (var ins = new JsonTextReader(new StringReader(json))) + { + return serializer.Deserialize(ins); + } + } + return null; + } + + private void SetEntryConfig(PwEntry e, KeePassHttpEntryConfig c) + { + var serializer = NewJsonSerializer(); + var writer = new StringWriter(); + serializer.Serialize(writer, c); + e.Strings.Set(KEEPASSHTTP_NAME, new ProtectedString(false, writer.ToString())); + e.Touch(true); + UpdateUI(e.ParentGroup); + } + + private bool UpdateEntry(PwUuid uuid, string username, string password, string formHost, string requestId) + { + PwEntry entry = null; + + var configOpt = new ConfigOpt(this.host.CustomConfig); + if (configOpt.SearchInAllOpenedDatabases) + { + foreach (PwDocument doc in host.MainWindow.DocumentManager.Documents) + { + if (doc.Database.IsOpen) + { + entry = doc.Database.RootGroup.FindEntry(uuid, true); + if (entry != null) + { + break; + } + } + } + } + else + { + entry = host.Database.RootGroup.FindEntry(uuid, true); + } + + if (entry == null) + { + return false; + } + + string[] up = GetUserPass(entry); + var u = up[0]; + var p = up[1]; + + if (u != username || p != password) + { + bool allowUpdate = configOpt.AlwaysAllowUpdates; + + if (!allowUpdate) + { + host.MainWindow.Activate(); + + DialogResult result; + if (host.MainWindow.IsTrayed()) + { + result = MessageBox.Show( + String.Format("Do you want to update the information in {0} - {1}?", formHost, u), + "Update Entry", MessageBoxButtons.YesNo, + MessageBoxIcon.None, MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly); + } + else + { + result = MessageBox.Show( + host.MainWindow, + String.Format("Do you want to update the information in {0} - {1}?", formHost, u), + "Update Entry", MessageBoxButtons.YesNo, + MessageBoxIcon.Information, MessageBoxDefaultButton.Button1); + } + + + if (result == DialogResult.Yes) + { + allowUpdate = true; + } + } + + if (allowUpdate) + { + PwObjectList m_vHistory = entry.History.CloneDeep(); + entry.History = m_vHistory; + entry.CreateBackup(null); + + entry.Strings.Set(PwDefs.UserNameField, new ProtectedString(false, username)); + entry.Strings.Set(PwDefs.PasswordField, new ProtectedString(true, password)); + entry.Touch(true, false); + UpdateUI(entry.ParentGroup); + + return true; + } + } + + return false; + } + + private bool CreateEntry(string username, string password, string urlHost, string url, Request r, Aes aes) + { + string realm = null; + if (r.Realm != null) + realm = CryptoTransform(r.Realm, true, false, aes, CMode.DECRYPT); + + var root = host.Database.RootGroup; + var group = root.FindCreateGroup(KEEPASSHTTP_GROUP_NAME, false); + if (group == null) + { + group = new PwGroup(true, true, KEEPASSHTTP_GROUP_NAME, PwIcon.WorldComputer); + root.AddGroup(group, true); + UpdateUI(null); + } + + string submithost = null; + if (r.SubmitUrl != null) + submithost = GetHost(CryptoTransform(r.SubmitUrl, true, false, aes, CMode.DECRYPT)); + + string baseUrl = url; + // index bigger than https:// <-- this slash + if (baseUrl.LastIndexOf("/") > 9) + { + baseUrl = baseUrl.Substring(0, baseUrl.LastIndexOf("/") + 1); + } + + PwEntry entry = new PwEntry(true, true); + entry.Strings.Set(PwDefs.TitleField, new ProtectedString(false, urlHost)); + entry.Strings.Set(PwDefs.UserNameField, new ProtectedString(false, username)); + entry.Strings.Set(PwDefs.PasswordField, new ProtectedString(true, password)); + entry.Strings.Set(PwDefs.UrlField, new ProtectedString(true, baseUrl)); + + if ((submithost != null && urlHost != submithost) || realm != null) + { + var config = new KeePassHttpEntryConfig(); + if (submithost != null) + config.Allow.Add(submithost); + if (realm != null) + config.Realm = realm; + + var serializer = NewJsonSerializer(); + var writer = new StringWriter(); + serializer.Serialize(writer, config); + entry.Strings.Set(KEEPASSHTTP_NAME, new ProtectedString(false, writer.ToString())); + } + + group.AddEntry(entry, true); + UpdateUI(group); + + return true; + } +} +*/ +} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Http.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Http.java new file mode 100644 index 00000000..03d1de71 --- /dev/null +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Http.java @@ -0,0 +1,446 @@ +package org.linguafranca.pwdb.keepasshttp; + +/** + * @author jo + */ +public class Http { + public enum CMode { + ENCRYPT(true), DECRYPT(false); + private final boolean encrypt; + + CMode(boolean encrypt) { + this.encrypt = encrypt; + } + + public boolean getEncrypt() { + return encrypt; + } + } + +/* + public static class KeePassHttpExt + { + + /// + /// an arbitrarily generated uuid for the keepasshttp root entry + /// + public final byte[] KEEPASSHTTP_UUID = { + 0x34, (byte) 0x69, (byte) 0x7a, (byte) 0x40, (byte) 0x8a, (byte) 0x5b, (byte) 0x41, (byte) 0xc0, + (byte) 0x9f, (byte) 0x36, (byte) 0x89, (byte) 0x7d, (byte) 0x62, (byte) 0x3e, (byte) 0xcb, (byte) 0x31 + }; + + private final int DEFAULT_NOTIFICATION_TIME = 5000; + public final String KEEPASSHTTP_NAME = "KeePassHttp Settings"; + private final String KEEPASSHTTP_GROUP_NAME = "KeePassHttp Passwords"; + public final String ASSOCIATE_KEY_PREFIX = "AES Key: "; + private IPluginHost host; + private HttpListener listener; + public final int DEFAULT_PORT = 19455; + public final String DEFAULT_HOST = "localhost"; + /// + /// TODO make configurable + /// + private const String HTTP_SCHEME = "http://"; + //private const String HTTPS_PREFIX = "https://localhost:"; + //private int HTTPS_PORT = DEFAULT_PORT + 1; + private Thread httpThread; + private volatile boolean stopped = false; + Dictionary handlers = new Dictionary(); + + //public String UpdateUrl = ""; + public String UpdateUrl = "https://passifox.appspot.com/kph/latest-version.txt"; + + private SearchParameters MakeSearchParameters() + { + SearchParameters p = new SearchParameters(); + p.SearchInTitles = true; + p.RegularExpression = true; + p.SearchInGroupNames = false; + p.SearchInNotes = false; + p.SearchInOther = false; + p.SearchInPasswords = false; + p.SearchInTags = false; + p.SearchInUrls = true; + p.SearchInUserNames = false; + p.SearchInUuids = false; + return p; + } + + private String CryptoTransform(String input, bool base64in, bool base64out, Aes cipher, CMode mode) + { + byte[] bytes; + if (base64in) + bytes = decode64(input); + else + bytes = Encoding.UTF8.GetBytes(input); + + + using (var c = mode == CMode.ENCRYPT ? cipher.CreateEncryptor() : cipher.CreateDecryptor()) { + var buf = c.TransformFinalBlock(bytes, 0, bytes.Length); + return base64out ? encode64(buf) : Encoding.UTF8.GetString(buf); + } + } + + private PwEntry GetConfigEntry(bool create) + { + var root = host.Database.RootGroup; + var uuid = new PwUuid(KEEPASSHTTP_UUID); + var entry = root.FindEntry(uuid, false); + if (entry == null && create) + { + entry = new PwEntry(false, true); + entry.Uuid = uuid; + entry.Strings.Set(PwDefs.TitleField, new ProtectedString(false, KEEPASSHTTP_NAME)); + root.AddEntry(entry, true); + UpdateUI(null); + } + return entry; + } + + private int GetNotificationTime() + { + var time = DEFAULT_NOTIFICATION_TIME; + var entry = GetConfigEntry(false); + if (entry != null) + { + var s = entry.Strings.ReadSafe("Prompt Timeout"); + if (s != null && s.Trim() != "") + { + try + { + time = Int32.Parse(s) * 1000; + } + catch { } + } + } + + return time; + } + + private void ShowNotification(String text) + { + ShowNotification(text, null, null); + } + + private void ShowNotification(String text, EventHandler onclick) + { + ShowNotification(text, onclick, null); + } + + private void ShowNotification(String text, EventHandler onclick, EventHandler onclose) + { + MethodInvoker m = delegate + { + var notify = host.MainWindow.MainNotifyIcon; + if (notify == null) + return; + + EventHandler clicked = null; + EventHandler closed = null; + + clicked = delegate + { + notify.BalloonTipClicked -= clicked; + notify.BalloonTipClosed -= closed; + if (onclick != null) + onclick(notify, null); + }; + closed = delegate + { + notify.BalloonTipClicked -= clicked; + notify.BalloonTipClosed -= closed; + if (onclose != null) + onclose(notify, null); + }; + + //notify.BalloonTipIcon = ToolTipIcon.Info; + notify.BalloonTipTitle = "KeePassHttp"; + notify.BalloonTipText = text; + notify.ShowBalloonTip(GetNotificationTime()); + // need to add listeners after showing, or closed is sent right away + notify.BalloonTipClosed += closed; + notify.BalloonTipClicked += clicked; + }; + if (host.MainWindow.InvokeRequired) + host.MainWindow.Invoke(m); + else + m.Invoke(); + } + + public override bool Initialize(IPluginHost host) + { + var httpSupported = HttpListener.IsSupported; + this.host = host; + + var optionsMenu = new ToolStripMenuItem("KeePassHttp Options..."); + optionsMenu.Click += OnOptions_Click; + optionsMenu.Image = KeePassHttp.Properties.Resources.earth_lock; + //optionsMenu.Image = global::KeePass.Properties.Resources.B16x16_File_Close; + this.host.MainWindow.ToolsMenu.DropDownItems.Add(optionsMenu); + + if (httpSupported) + { + try + { + handlers.Add(Request.TEST_ASSOCIATE, TestAssociateHandler); + handlers.Add(Request.ASSOCIATE, AssociateHandler); + handlers.Add(Request.GET_LOGINS, GetLoginsHandler); + handlers.Add(Request.GET_LOGINS_COUNT, GetLoginsCountHandler); + handlers.Add(Request.GET_ALL_LOGINS, GetAllLoginsHandler); + handlers.Add(Request.SET_LOGIN, SetLoginHandler); + handlers.Add(Request.GENERATE_PASSWORD, GeneratePassword); + + listener = new HttpListener(); + + var configOpt = new ConfigOpt(this.host.CustomConfig); + + listener.Prefixes.Add(HTTP_SCHEME + configOpt.ListenerHost + ":" + configOpt.ListenerPort.ToString() + "/"); + //listener.Prefixes.Add(HTTPS_PREFIX + HTTPS_PORT + "/"); + listener.Start(); + + httpThread = new Thread(new ThreadStart(Run)); + httpThread.Start(); + } catch (HttpListenerException e) { + MessageBox.Show(host.MainWindow, + "Unable to start HttpListener!\nDo you really have only one installation of KeePassHttp in your KeePass-directory?\n\n" + e, + "Unable to start HttpListener", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + } + } + else + { + MessageBox.Show(host.MainWindow, "The .NET HttpListener is not supported on your OS", + ".NET HttpListener not supported", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + } + return httpSupported; + } + + void OnOptions_Click(object sender, EventArgs e) + { + var form = new OptionsForm(new ConfigOpt(host.CustomConfig)); + UIUtil.ShowDialogAndDestroy(form); + } + + private void Run() + { + while (!stopped) + { + try + { + var r = listener.BeginGetContext(new AsyncCallback(RequestHandler), listener); + r.AsyncWaitHandle.WaitOne(); + r.AsyncWaitHandle.Close(); + } + catch (ThreadInterruptedException) { } + catch (HttpListenerException e) { + MessageBox.Show(host.MainWindow, "Unable to process request!\n\n" + e, + "Unable to process request", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + } + } + } + + private JsonSerializer NewJsonSerializer() + { + var settings = new JsonSerializerSettings(); + settings.DefaultValueHandling = DefaultValueHandling.Ignore; + settings.NullValueHandling = NullValueHandling.Ignore; + + return JsonSerializer.Create(settings); + } + private Response ProcessRequest(Request r, HttpListenerResponse resp) + { + String hash = host.Database.RootGroup.Uuid.ToHexString() + host.Database.RecycleBinUuid.ToHexString(); + hash = getSHA1(hash); + + var response = new Response(r.RequestType, hash); + + using (var aes = new AesManaged()) + { + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + var handler = handlers[r.RequestType]; + if (handler != null) + { + try + { + handler(r, response, aes); + } + catch (Exception e) + { + ShowNotification("***BUG*** " + e, (s,evt) => MessageBox.Show(host.MainWindow, e + "")); + response.Error = e + ""; + resp.StatusCode = (int)HttpStatusCode.BadRequest; + } + } + else + { + response.Error = "Unknown command: " + r.RequestType; + resp.StatusCode = (int)HttpStatusCode.BadRequest; + } + } + + return response; + } + private void RequestHandler(IAsyncResult r) + { + try { + _RequestHandler(r); + } catch (Exception e) { + MessageBox.Show(host.MainWindow, "RequestHandler failed: " + e); + } + } + private void _RequestHandler(IAsyncResult r) + { + if (stopped) return; + var l = (HttpListener)r.AsyncState; + var ctx = l.EndGetContext(r); + var req = ctx.Request; + var resp = ctx.Response; + + var serializer = NewJsonSerializer(); + Request request = null; + + resp.StatusCode = (int)HttpStatusCode.OK; + using (var ins = new JsonTextReader(new StreamReader(req.InputStream))) + { + try + { + request = serializer.Deserialize(ins); + } + catch (JsonSerializationException e) + { + var buffer = Encoding.UTF8.GetBytes(e + ""); + resp.StatusCode = (int)HttpStatusCode.BadRequest; + resp.ContentLength64 = buffer.Length; + resp.OutputStream.Write(buffer, 0, buffer.Length); + } // ignore, bad request + } + + var db = host.Database; + + var configOpt = new ConfigOpt(this.host.CustomConfig); + + if (request != null && (configOpt.UnlockDatabaseRequest || request.TriggerUnlock == "true") && !db.IsOpen) + { + host.MainWindow.Invoke((MethodInvoker)delegate + { + host.MainWindow.EnsureVisibleForegroundWindow(true, true); + }); + + // UnlockDialog not already opened + bool bNoDialogOpened = (KeePass.UI.GlobalWindowManager.WindowCount == 0); + if (!db.IsOpen && bNoDialogOpened) + { + host.MainWindow.Invoke((MethodInvoker)delegate + { + host.MainWindow.OpenDatabase(host.MainWindow.DocumentManager.ActiveDocument.LockedIoc, null, false); + }); + } + } + + if (request != null && db.IsOpen) + { + Response response = null; + if (request != null) + response = ProcessRequest(request, resp); + + resp.ContentType = "application/json"; + var writer = new StringWriter(); + if (response != null) + { + serializer.Serialize(writer, response); + var buffer = Encoding.UTF8.GetBytes(writer.ToString()); + resp.ContentLength64 = buffer.Length; + resp.OutputStream.Write(buffer, 0, buffer.Length); + } + } + else + { + resp.StatusCode = (int)HttpStatusCode.ServiceUnavailable; + } + + var outs = resp.OutputStream; + outs.Close(); + resp.Close(); + } + + public override void Terminate() + { + stopped = true; + listener.Stop(); + listener.Close(); + httpThread.Interrupt(); + } + + private void UpdateUI(PwGroup group) + { + var win = host.MainWindow; + if (group == null) group = host.Database.RootGroup; + var f = (MethodInvoker) delegate { + win.UpdateUI(false, null, true, group, true, null, true); + }; + if (win.InvokeRequired) + win.Invoke(f); + else + f.Invoke(); + } + + internal String[] GetUserPass(PwEntry entry) + { + return GetUserPass(new PwEntryDatabase(entry, host.Database)); + } + + internal String[] GetUserPass(PwEntryDatabase entryDatabase) + { + // follow references + SprContext ctx = new SprContext(entryDatabase.entry, entryDatabase.database, + SprCompileFlags.All, false, false); + String user = SprEngine.Compile( + entryDatabase.entry.Strings.ReadSafe(PwDefs.UserNameField), ctx); + String pass = SprEngine.Compile( + entryDatabase.entry.Strings.ReadSafe(PwDefs.PasswordField), ctx); + var f = (MethodInvoker)delegate + { + // apparently, SprEngine.Compile might modify the database + host.MainWindow.UpdateUI(false, null, false, null, false, null, false); + }; + if (host.MainWindow.InvokeRequired) + host.MainWindow.Invoke(f); + else + f.Invoke(); + + return new String[] { user, pass }; + } + + /// + /// Liefert den SHA1 Hash + /// + /// EingabeString + /// SHA1 Hash der EingabeStrings + private String getSHA1(String input) + { + //Umwandlung des EingaString in den SHA1 Hash + System.Security.Cryptography.SHA1 sha1 = new System.Security.Cryptography.SHA1CryptoServiceProvider(); + byte[] textToHash = Encoding.Default.GetBytes(input); + byte[] result = sha1.ComputeHash(textToHash); + + //SHA1 Hash in String konvertieren + System.Text.StringBuilder s = new System.Text.StringBuilder(); + foreach (byte b in result) + { + s.Append(b.ToString("x2").ToLower()); + } + + return s.ToString(); + } +} +*/ +} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java new file mode 100644 index 00000000..d62a6ff6 --- /dev/null +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java @@ -0,0 +1,71 @@ +package org.linguafranca.pwdb.keepasshttp; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.apache.commons.codec.binary.Hex; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.linguafranca.pwdb.kdbx.Helpers; +import org.linguafranca.pwdb.keepasshttp.util.LogginInputStream; +import org.linguafranca.pwdb.keepasshttp.util.LogginOutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongycastle.crypto.digests.SHA1Digest; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.util.UUID; + +/** + * @author jo + */ +public class KeePassJavaHttpHandler extends AbstractHandler { + private Logger logger = LoggerFactory.getLogger(KeePassJavaHttpHandler.class); + private Gson gson = new GsonBuilder().disableHtmlEscaping().create(); + Processor processor = new Processor(); + + @Override + public void handle(String s, Request request, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException, ServletException { + + logger.debug("Got a request"); + InputStream is = new LogginInputStream(request.getInputStream(), logger); + + Message.Request request1 = gson.fromJson(new BufferedReader(new InputStreamReader(is)),Message.Request.class); + if (request1 == null) { + httpServletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); + request.setHandled(true); + httpServletResponse.getWriter().write("That's a 400. JSON not parsed. " + request.getRemoteAddr()); + return; + } + if (request1.RequestType == null) { + httpServletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); + request.setHandled(true); + httpServletResponse.getWriter().write("That's a 400. No request type found. " + request.getRemoteAddr()); + return; + } + + Message.Response response = new Message.Response(request1.RequestType, processor.getHash()); + Processor.RequestHandler handler = processor.getHandler(request1.RequestType); + + if (handler == null) { + httpServletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); + request.setHandled(true); + response.Success = false; + response.Error = "No valid request type found " + request1.RequestType; + httpServletResponse.getWriter().write(gson.toJson(response)); + return; + } + + handler.process(request1, response); + + OutputStream outputStream = new LogginOutputStream(httpServletResponse.getOutputStream(), logger); + Writer writer = new BufferedWriter(new OutputStreamWriter(outputStream)); + gson.toJson(response, writer); + writer.flush(); + httpServletResponse.setStatus(HttpServletResponse.SC_OK); + request.setHandled(true); + } + +} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaJettyServer.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaJettyServer.java new file mode 100644 index 00000000..129c7c9d --- /dev/null +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaJettyServer.java @@ -0,0 +1,23 @@ +package org.linguafranca.pwdb.keepasshttp; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; + +/** + * @author jo + */ +public class KeePassJavaJettyServer { + public static void main( String[] args ) throws Exception + { + Server server = new Server(); + ServerConnector http = new ServerConnector(server); + http.setHost("0.0.0.0"); + //http.setPort(19455); + http.setPort(19455); + http.setIdleTimeout(300000); + server.addConnector(http); + server.setHandler(new KeePassJavaHttpHandler()); + server.start(); + server.join(); + } +} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java new file mode 100644 index 00000000..9d4c834e --- /dev/null +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java @@ -0,0 +1,179 @@ +package org.linguafranca.pwdb.keepasshttp; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +/** + * @author jo + */ +public class Message { + + public static final String VERSION = "1.8.4.1"; + + public static final class Type { + public static final String GET_LOGINS = "get-logins"; + public static final String GET_LOGINS_COUNT = "get-logins-count"; + public static final String GET_ALL_LOGINS = "get-all-logins"; + public static final String SET_LOGIN = "set-login"; + public static final String ASSOCIATE = "associate"; + public static final String TEST_ASSOCIATE = "test-associate"; + public static final String GENERATE_PASSWORD = "generate-password"; + + } + + public static class Verifiable { + /** + * Nonce value used in conjunction with all encrypted fields, + * randomly generated for each request + */ + public String Nonce; + + /** + * Used to check that the correct key has been chosen + */ + public String Verifier; + } + + public static class Request extends Verifiable { + + public String RequestType; + + /** + * Sort selection by best URL matching for given hosts + */ + public String SortSelection; + + /** + * Trigger unlock of database even if feature is disabled in KPH (because of user interaction to fill-in) + */ + public String TriggerUnlock; + + /** + * Always encrypted, used with set-login, uuid is set + * if modifying an existing login + */ + public String Login; + public String Password; + public String Uuid; + + /** + * Always encrypted, used with get and set-login + */ + public String Url; + + /** + * Always encrypted, used with get-login + */ + public String SubmitUrl; + + /** + * Send the AES key ID with the 'associate' request + */ + public String Key; + + /** + * Always required, an identifier given by the KeePass user + */ + public String Id; + + /** + * Realm value used for filtering results. Always encrypted. + */ + public String Realm; + } + + public static class Response extends Verifiable { + public Response(String request, String hash) { + RequestType = request; + + if (request.equals(Type.GET_LOGINS) || + request.equals(Type.GET_ALL_LOGINS) || + request.equals(Type.GENERATE_PASSWORD)) + Entries = new ArrayList<>(); + else + Entries = null; + + this.Hash = hash; + this.Version = VERSION; + } + + /** + * Mirrors the request type of KeePassRequest + */ + public String RequestType; + + public String Error = null; + + public boolean Success = false; + + /** + * The user selected String as a result of 'associate', + * always returned on every request + */ + public String Id; + + /** + * response to get-logins-count, number of entries for requested Url + */ + public int Count = 0; + + /** + * response the current version of KeePassHttp + */ + public String Version = ""; + + /** + * response an unique hash of the database composed of RootGroup UUid and RecycleBin UUid + */ + public String Hash = ""; + + /** + * The resulting entries for a get-login request + */ + public List Entries; + } + + public static class ResponseEntry { + public ResponseEntry() { + } + + public ResponseEntry(String name, String login, String password, String uuid) { + this(name, login, password, uuid, new ArrayList()); + } + + public ResponseEntry(String name, String login, String password, String uuid, List StringFields) { + this.Login = login; + this.Password = password; + this.Uuid = uuid; + this.Name = name; + this.StringFields = StringFields; + } + + public String Login; + public String Password; + public String Uuid; + public String Name; + public List StringFields = null; + + } + + public static class ResponseStringField { + public ResponseStringField() { + } + + public ResponseStringField(String key, String value) { + Key = key; + Value = value; + } + + public String Key; + public String Value; + } + + static class KeePassHttpEntryConfig { + public HashSet Allow = new HashSet(); + public HashSet Deny = new HashSet(); + public String Realm = null; + } +} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java new file mode 100644 index 00000000..4227452e --- /dev/null +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java @@ -0,0 +1,360 @@ +package org.linguafranca.pwdb.keepasshttp; + +import org.apache.commons.codec.binary.Hex; +import org.linguafranca.pwdb.kdbx.Helpers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongycastle.crypto.InvalidCipherTextException; +import org.spongycastle.crypto.digests.SHA1Digest; +import org.spongycastle.crypto.engines.AESFastEngine; +import org.spongycastle.crypto.modes.CBCBlockCipher; +import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.spongycastle.crypto.params.KeyParameter; +import org.spongycastle.crypto.params.ParametersWithIV; + +import java.security.SecureRandom; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * @author jo + */ +public class Processor { + + private byte[] binaryKey; + private String id; + private Map handlers = new HashMap(); + + private Logger logger = LoggerFactory.getLogger(this.getClass()); + private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + + private String makeId() { + return id = "Today's ID is " + dateFormat.format(new Date()); + } + + private UUID rootGroupUuid = UUID.randomUUID(); + private UUID recycleBinUuid = UUID.randomUUID(); + + public String getHash() { + byte[] toHash = (Helpers.hexStringFromUuid(rootGroupUuid) + Helpers.hexStringFromUuid(recycleBinUuid)).getBytes(); + SHA1Digest digest = new SHA1Digest(); + byte[] digestBytes = new byte[digest.getDigestSize()]; + digest.update(toHash, 0, toHash.length); + digest.doFinal(digestBytes, 0); + String result = new String(Hex.encodeHex(digestBytes)); + return result.toLowerCase(); + } + + public interface RequestHandler { + void process(Message.Request request, Message.Response response); + } + + public RequestHandler getHandler(String requestType) { + return handlers.get(requestType); + } + + public Processor(byte[] binaryKey) { + this(); + this.binaryKey = binaryKey; + } + + public Processor(String base64Key) { + this(Helpers.decodeBase64Content(base64Key.getBytes())); + } + + public Processor() { + handlers.put(Message.Type.TEST_ASSOCIATE, new TestAssociateHandler()); + handlers.put(Message.Type.ASSOCIATE, new AssociateHandler()); + handlers.put(Message.Type.GET_LOGINS, new GetLoginsHandler()); +/* + handlers.put(Message.Request.GET_LOGINS_COUNT, GetLoginsCountHandler); + handlers.put(Message.Request.GET_ALL_LOGINS, GetAllLoginsHandler); + handlers.put(Message.Request.SET_LOGIN, SetLoginHandler); + handlers.put(Message.Request.GENERATE_PASSWORD, GeneratePassword); +*/ + } + + public PaddedBufferedBlockCipher getCipher(Http.CMode mode, byte[] iv) { + PaddedBufferedBlockCipher result = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); + result.init(mode.getEncrypt(), new ParametersWithIV(new KeyParameter(getKey()), iv)); + return result; + } + + public boolean verify(Message.Verifiable verifiable) { + byte[] verifier = Helpers.decodeBase64Content(verifiable.Verifier.getBytes(), false); + byte[] iv = Helpers.decodeBase64Content(verifiable.Nonce.getBytes(), false); + + PaddedBufferedBlockCipher cipher = getCipher(Http.CMode.DECRYPT, iv); + + byte[] output = new byte[cipher.getOutputSize(verifier.length)]; + int outputlen = cipher.processBytes(verifier, 0, verifier.length, output, 0); + + try { + cipher.doFinal(output, outputlen); + byte[] comparison = new byte[output.length]; + System.arraycopy(verifiable.Nonce.getBytes(),0,comparison,0,verifiable.Nonce.length()); + return Arrays.equals(output, comparison); + } catch (InvalidCipherTextException e) { + return false; + } + } + + public void makeVerifiable(Message.Response response) { + byte[] iv = new SecureRandom().generateSeed(16); + response.Nonce = Helpers.encodeBase64Content(iv, false); + PaddedBufferedBlockCipher cipher = getCipher(Http.CMode.ENCRYPT, iv); + response.Verifier = CryptoTransform(response.Nonce, false, true, cipher); + + if (response.RequestType.equals(Message.Type.GET_LOGINS)) { + for (Message.ResponseEntry entry: response.Entries) { + entry.Login = encryptToBase64(entry.Login, response); + entry.Uuid = encryptToBase64(entry.Uuid, response); + entry.Name = encryptToBase64(entry.Name, response); + entry.Password = encryptToBase64(entry.Password, response); + } + } + } + + public String decryptFromBase64(String input, Message.Verifiable verifiable){ + return CryptoTransform(input, true, false, getCipher(Http.CMode.DECRYPT, Helpers.decodeBase64Content(verifiable.Nonce.getBytes()))); + } + + public String encryptToBase64(String input, Message.Verifiable verifiable){ + return CryptoTransform(input, false, true, getCipher(Http.CMode.ENCRYPT, Helpers.decodeBase64Content(verifiable.Nonce.getBytes()))); + } + + public String CryptoTransform(String input, boolean base64in, boolean base64out, PaddedBufferedBlockCipher cipher) { + byte[] bytes; + if (base64in) { + bytes = Helpers.decodeBase64Content(input.getBytes(), false); + } else { + bytes = input.getBytes(); + } + + byte[] output = new byte[cipher.getOutputSize(bytes.length)]; + int outputlen = cipher.processBytes(bytes, 0, bytes.length, output, 0); + try { + int len = cipher.doFinal(output, outputlen); + // padded buffer is required on bas64 i.e. encrypted direction + if (base64out) { + return Helpers.encodeBase64Content(output, false); + } + // trim to buffer length + return new String(output, 0, outputlen + len); + } catch (InvalidCipherTextException e) { + throw new IllegalStateException(e); + } + } + + private class AssociateHandler implements RequestHandler { + @Override + public void process(Message.Request request, Message.Response response) { + Processor.this.binaryKey = Helpers.decodeBase64Content(request.Key.getBytes()); + if (!verify(request)) { + logger.warn("Verification failed for " + request.RequestType); + return; + } + + response.Id = makeId(); + response.Success = true; + makeVerifiable(response); + } + } + + private class TestAssociateHandler implements RequestHandler { + @Override + public void process(Message.Request request, Message.Response response) { + if (request.Verifier == null || request.Nonce == null) { + return; + } + if (!verify(request)) { + logger.warn("Verification failed for " + request.RequestType); + return; + } + response.Success = false; + if (request.Id != null) { + response.Success = request.Id.equals(id); + } + makeVerifiable(response); + } + } + + private class GetLoginsHandler implements Processor.RequestHandler { + public void process(Message.Request r, Message.Response resp) { + if (!verify(r)) { + logger.warn("Verification failed for " + r.RequestType); + return; + } + + resp.Entries.add(new Message.ResponseEntry("FB", "FBLOGIN", "FBPASS", + Helpers.base64FromUuid(UUID.randomUUID()))); + resp.Entries.add(new Message.ResponseEntry("FB2", "FBLOGIN2", "FBPASS2", + Helpers.base64FromUuid(UUID.randomUUID()))); + resp.Id = id; + resp.Success = true; + resp.Count = 0; + makeVerifiable(resp); + +/* + string submithost = null; + var host = GetHost(CryptoTransform(r.Url, true, false, aes, CMode.DECRYPT)); + if (r.SubmitUrl != null) + submithost = GetHost(CryptoTransform(r.SubmitUrl, true, false, aes, CMode.DECRYPT)); + + var items = FindMatchingEntries(r, aes); + if (items.ToList().Count > 0) { + Func filter = delegate(PwEntry e) + { + var c = GetEntryConfig(e); + + var title = e.Strings.ReadSafe(PwDefs.TitleField); + var entryUrl = e.Strings.ReadSafe(PwDefs.UrlField); + if (c != null) { + return title != host && entryUrl != host && !c.Allow.Contains(host) || (submithost != null && !c.Allow.Contains(submithost) && submithost != title && submithost != entryUrl); + } + return title != host && entryUrl != host || (submithost != null && title != submithost && entryUrl != submithost); + } + ; + + var configOpt = new ConfigOpt(this.host.CustomConfig); + var config = GetConfigEntry(true); + var autoAllowS = config.Strings.ReadSafe("Auto Allow"); + var autoAllow = autoAllowS != null && autoAllowS.Trim() != ""; + autoAllow = autoAllow || configOpt.AlwaysAllowAccess; + var needPrompting = from e in items where filter(e.entry) select e; + + if (needPrompting.ToList().Count > 0 && !autoAllow) { + var win = this.host.MainWindow; + + using(var f = new AccessControlForm()) + { + win.Invoke((MethodInvoker) delegate + { + f.Icon = win.Icon; + f.Plugin = this; + f.Entries = (from e in items where filter(e.entry) select e.entry).ToList(); + //f.Entries = needPrompting.ToList(); + f.Host = submithost != null ? submithost : host; + f.Load += delegate { + f.Activate(); + } + ; + f.ShowDialog(win); + if (f.Remember && (f.Allowed || f.Denied)) { + foreach(var e in needPrompting) + { + var c = GetEntryConfig(e.entry); + if (c == null) + c = new KeePassHttpEntryConfig(); + var set = f.Allowed ? c.Allow : c.Deny; + set.Add(host); + if (submithost != null && submithost != host) + set.Add(submithost); + SetEntryConfig(e.entry, c); + + } + } + if (!f.Allowed) { + items = items.Except(needPrompting); + } + }); + } + } + + string compareToUrl = null; + if (r.SubmitUrl != null) { + compareToUrl = CryptoTransform(r.SubmitUrl, true, false, aes, CMode.DECRYPT); + } + if (String.IsNullOrEmpty(compareToUrl)) + compareToUrl = CryptoTransform(r.Url, true, false, aes, CMode.DECRYPT); + + compareToUrl = compareToUrl.ToLower(); + + foreach(var entryDatabase in items) + { + string entryUrl = String.Copy(entryDatabase.entry.Strings.ReadSafe(PwDefs.UrlField)); + if (String.IsNullOrEmpty(entryUrl)) + entryUrl = entryDatabase.entry.Strings.ReadSafe(PwDefs.TitleField); + + entryUrl = entryUrl.ToLower(); + + entryDatabase.entry.UsageCount = (ulong) LevenshteinDistance(compareToUrl, entryUrl); + + } + + var itemsList = items.ToList(); + + if (configOpt.SpecificMatchingOnly) { + itemsList = (from e in itemsList + orderby e.entry.UsageCount ascending + select e).ToList(); + + ulong lowestDistance = itemsList.Count > 0 ? + itemsList[0].entry.UsageCount : + 0; + + itemsList = (from e in itemsList + where e.entry.UsageCount == lowestDistance + orderby e.entry.UsageCount + select e).ToList(); + + } + + if (configOpt.SortResultByUsername) { + var items2 = from e in itemsList orderby e.entry.UsageCount ascending, GetUserPass (e)[0] + ascending select e; + itemsList = items2.ToList(); + } else { + var items2 = from e in itemsList orderby e.entry.UsageCount ascending, e. + entry.Strings.ReadSafe(PwDefs.TitleField) ascending select e; + 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 < string > ()); + + 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; + } else { + resp.Success = true; + resp.Id = r.Id; + SetResponseVerifier(resp, aes); + } +*/ + } + } + + private byte[] getKey() { + return binaryKey; + } +} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginInputStream.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginInputStream.java new file mode 100644 index 00000000..a5d39b4d --- /dev/null +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginInputStream.java @@ -0,0 +1,72 @@ +package org.linguafranca.pwdb.keepasshttp.util; + +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.InputStream; + +/** + * @author jo + */ +public class LogginInputStream extends InputStream { + + InputStream is; + Logger logger; + + public LogginInputStream(InputStream is, Logger logger) { + this.is = is; + this.logger = logger; + } + + @Override + public int read() throws IOException { + int i = is.read(); + return is.read(); + } + + @Override + public int read(byte[] b) throws IOException { + return is.read(b); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + + int i = is.read(b, off, len); + if (i>=0) { + String s = new String(b, off, i); + logger.info("--> " + s); + } + return i; + } + + @Override + public long skip(long n) throws IOException { + return is.skip(n); + } + + @Override + public int available() throws IOException { + return is.available(); + } + + @Override + public void close() throws IOException { + is.close(); + } + + @Override + public void mark(int readlimit) { + is.mark(readlimit); + } + + @Override + public void reset() throws IOException { + is.reset(); + } + + @Override + public boolean markSupported() { + return is.markSupported(); + } +} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginOutputStream.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginOutputStream.java new file mode 100644 index 00000000..2bec2c60 --- /dev/null +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginOutputStream.java @@ -0,0 +1,46 @@ +package org.linguafranca.pwdb.keepasshttp.util; + +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * @author jo + */ +public class LogginOutputStream extends OutputStream { + + Logger logger; + OutputStream outputStream; + + public LogginOutputStream(OutputStream outputStream, Logger logger) { + this.logger = logger; + this.outputStream = outputStream; + } + + @Override + public void write(int b) throws IOException { + outputStream.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + outputStream.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + logger.info("<-- " + new String(b, off, len)); + outputStream.write(b, off, len); + } + + @Override + public void flush() throws IOException { + outputStream.flush(); + } + + @Override + public void close() throws IOException { + outputStream.close(); + } +} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginProxy.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginProxy.java new file mode 100644 index 00000000..32b48b4f --- /dev/null +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginProxy.java @@ -0,0 +1,111 @@ +package org.linguafranca.pwdb.keepasshttp.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author jo + */ +public class LogginProxy { + private static final AtomicInteger count = new AtomicInteger(0); + + public static Logger logger = LoggerFactory.getLogger("proxy"); + + public LogginProxy(){ + + } + + public static void main(String[] args) throws IOException { + go(); + } + + public static void go() throws IOException { + ServerSocket serverSocket = new ServerSocket(19455); + do { + logger.info("Awaiting connection"); + Socket clientSocket = serverSocket.accept(); + Executors.newSingleThreadExecutor().submit(new Service(clientSocket)); + }while (true); + + } + + private static class Service implements Callable { + private final Socket clientSocket; + + private Service(Socket socket) { + this.clientSocket = socket; + } + + @Override + public Boolean call() throws Exception { + final String threadName = "Connection " + count.getAndIncrement(); + Thread.currentThread().setName(threadName); + final InputStream serverInputStream = clientSocket.getInputStream(); + final OutputStream serverOutputStream = clientSocket.getOutputStream(); + + Socket forwardSocket = new Socket("192.168.1.131", 19456); + final InputStream clientInputStream = forwardSocket.getInputStream(); + final OutputStream clientOutputStream = forwardSocket.getOutputStream(); + + Callable upstream = new Callable() { + @Override + public Boolean call() throws Exception { + Thread.currentThread().setName(threadName + " ->"); + logger.info("upstream starting"); + byte[] b = new byte[1024]; + int l; + while ((l=serverInputStream.read(b)) > -1) { + clientOutputStream.write(b,0,l); + clientOutputStream.flush(); + logger.info(new String(b, 0, l)); + } + logger.info("upstream finished"); + return true; + } + }; + + Callable downstream = new Callable() { + @Override + public Boolean call() throws Exception { + Thread.currentThread().setName(threadName + " <-"); + logger.info("downstream starting"); + byte[] b = new byte[1024]; + int l; + while ((l = clientInputStream.read(b)) > -1) { + serverOutputStream.write(b,0,l); + serverOutputStream.flush(); + logger.info(new String(b, 0, l)); + } + logger.info("downstream finished"); + return true; + } + }; + Future upstreamFuture = Executors.newSingleThreadExecutor().submit(upstream); + Future downStreamFuture = Executors.newSingleThreadExecutor().submit(downstream); + try { + upstreamFuture.get(); + //forwardSocket.shutdownOutput(); + downStreamFuture.get(); + forwardSocket.shutdownOutput(); + clientSocket.shutdownOutput(); + logger.info("Connection finished"); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + return true; + } + } +} diff --git a/http/src/main/resources/logback.xml b/http/src/main/resources/logback.xml new file mode 100644 index 00000000..afaebf8e --- /dev/null +++ b/http/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/http/src/test/java/MonitoredRequestsTest.java b/http/src/test/java/MonitoredRequestsTest.java new file mode 100644 index 00000000..83177fc8 --- /dev/null +++ b/http/src/test/java/MonitoredRequestsTest.java @@ -0,0 +1,76 @@ +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.junit.Test; +import org.linguafranca.pwdb.keepasshttp.Message; +import org.linguafranca.pwdb.keepasshttp.Processor; + +import static org.junit.Assert.assertEquals; + +/** + * @author jo + */ +public class MonitoredRequestsTest { + private static String associateRequest = "{\"RequestType\":\"associate\",\"Key\":\"NTxz7Rej/a4H9OFXBmXxMp0BjIfqhy5QEYjLnqhWdkA=\",\"Nonce\":\"UJUjvviWfVDK7c6KzZNUkw==\",\"Verifier\":\"f3DwAdI7rcWAH0DzGlCMTjLtH2q2+eBLNgRuojQWLS8=\"}"; + private static String associateResponse = "{\"RequestType\":\"associate\",\"Success\":true,\"Id\":\"NEW coNECTION\",\"Count\":0,\"Version\":\"1.8.4.1\",\"Hash\":\"eefc4faf792a59d034da69c69f3643ef34ba7d8d\",\"Nonce\":\"/cdtpNsn3sjiwVtrwYmx3g==\",\"Verifier\":\"5onGXGq18yT4BM3QJnqUxXMKJ7/4fnRvjZ6R0OO0RSE=\"}"; + + private static String testAssociateRequestWithId = "{\"RequestType\":\"test-associate\",\"TriggerUnlock\":false,\"Id\":\"NEW coNECTION\",\"Nonce\":\"gWgoWCbp6/9YsuvVH3hSPw==\",\"Verifier\":\"NQ0d51+hW4Y7NBB0Ns6QKPg0BSWnHwHzleMKpZsdO54=\"}"; + private static String testAssociateResponseWithId = "{\"RequestType\":\"test-associate\",\"Success\":true,\"Id\":\"NEW coNECTION\",\"Count\":0,\"Version\":\"1.8.4.1\",\"Hash\":\"eefc4faf792a59d034da69c69f3643ef34ba7d8d\",\"Nonce\":\"HMtkcQjmeRxxm8/GLcPsAQ==\",\"Verifier\":\"t+wp+yIoG497STtDUf7st9yGSLStHglihAO+GMFHnzg=\"}"; + + private static String getLoginsRequest = "{\"RequestType\":\"get-logins\",\"SortSelection\":\"true\",\"TriggerUnlock\":\"false\",\"Id\":\"NEW coNECTION\",\"Nonce\":\"79y7EyDVndGn3B5iFLp4Kw==\",\"Verifier\":\"LIY9XDHscI+xgrzIVCFnqsW9yigiNzWbJ1ZpOIBKGVs=\",\"Url\":\"eZpW5dUli05H5l1+MuwiUNjXZyNemZDmQOFjgkz8uvg=\",\"SubmitUrl\":\"eZpW5dUli05H5l1+MuwiUO2/H/Faw6yvjvgrCElBRSSbGRJ3a4hXFdq0ws75fK26XT9SQQXqwOvCiTiK5Ybnxg==\"}"; + private static String getLoginsResponse = "{\"RequestType\":\"get-logins\",\"Success\":true,\"Id\":\"NEW coNECTION\",\"Count\":1,\"Version\":\"1.8.4.1\",\"Hash\":\"eefc4faf792a59d034da69c69f3643ef34ba7d8d\",\"Nonce\":\"hQ2JSllJLrVWv1LcxN8+rQ==\",\"Verifier\":\"TT2014GvCXk+bruePydwqiGFeupP+xv7F7UnC5lc7vo=\",\"Entries\":[{\"Login\":\"n1qi36EQkTQhzQiT7TFonA==\",\"Password\":\"gvgmfbelEa3QxvzRsEG0Hg==\",\"Uuid\":\"w5MJa4za3DLh+2CmbG9Yl5kr5Gba0wnptN24kZPsWHPA8hPx6Pb1XYxiseh5gARY\",\"Name\":\"xpCqAwBjmUOqVyI6dfJqGA==\"}]}"; + + Gson gson = new GsonBuilder().disableHtmlEscaping().create(); + + + @Test + public void sessionTest() { + Processor processor = new Processor(); + + Message.Request request = gson.fromJson(associateRequest, Message.Request.class); + Message.Response actualResponse = gson.fromJson(associateResponse, Message.Response.class); + + Message.Response computedResponse = new Message.Response(request.RequestType, "eefc4faf792a59d034da69c69f3643ef34ba7d8d"); + processor.getHandler(request.RequestType).process(request, computedResponse); + computedResponse.Id = "NEW coNECTION"; + + System.out.println(gson.toJson(computedResponse)); + processor.verify(computedResponse); + System.out.println(gson.toJson(actualResponse)); + processor.verify(actualResponse); + + + + request = gson.fromJson(testAssociateRequestWithId, Message.Request.class); + actualResponse = gson.fromJson(testAssociateResponseWithId, Message.Response.class); + + computedResponse = new Message.Response(request.RequestType, "eefc4faf792a59d034da69c69f3643ef34ba7d8d"); + processor.getHandler(request.RequestType).process(request, computedResponse); + computedResponse.Id = "NEW coNECTION"; + + System.out.println(gson.toJson(computedResponse)); + processor.verify(computedResponse); + System.out.println(gson.toJson(actualResponse)); + processor.verify(actualResponse); + + + request = gson.fromJson(getLoginsRequest, Message.Request.class); + actualResponse = gson.fromJson(getLoginsResponse, Message.Response.class); + + computedResponse = new Message.Response(request.RequestType, "eefc4faf792a59d034da69c69f3643ef34ba7d8d"); + processor.getHandler(request.RequestType).process(request, computedResponse); + computedResponse.Id = "NEW coNECTION"; + + System.out.println(gson.toJson(computedResponse)); + processor.verify(computedResponse); + System.out.println(gson.toJson(actualResponse)); + processor.verify(actualResponse); + for (Message.ResponseEntry entry: actualResponse.Entries) { + System.out.println(processor.decryptFromBase64(entry.Login, actualResponse)); + System.out.println(processor.decryptFromBase64(entry.Name, actualResponse)); + System.out.println(processor.decryptFromBase64(entry.Password, actualResponse)); + System.out.println(processor.decryptFromBase64(entry.Uuid, actualResponse)); + } + + + } +} diff --git a/http/src/test/java/TestRequestResponse.java b/http/src/test/java/TestRequestResponse.java new file mode 100644 index 00000000..4421c809 --- /dev/null +++ b/http/src/test/java/TestRequestResponse.java @@ -0,0 +1,68 @@ +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.junit.Test; +import org.linguafranca.pwdb.kdbx.Helpers; +import org.linguafranca.pwdb.keepasshttp.Http; +import org.linguafranca.pwdb.keepasshttp.Processor; +import org.linguafranca.pwdb.keepasshttp.Message; +import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; + +import java.util.ArrayList; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author jo + */ +public class TestRequestResponse { + private static String requestString = "{\"RequestType\":\"associate\",\"Key\":\"WQISYVajkviMNaSBIaUTgZbUaJD1RujU6eoh3B89+lc=\",\"Nonce\":\"V2bJ4VFRrbH+oUd0W8swjQ==\",\"Verifier\":\"oegZmDzOLFVv8jAYxfnOqBk+0KiasSr6J/8v9xbUIkE=\"}"; + private static String getLoginsAssociate ="{\"RequestType\":\"associate\",\"Key\":\"W4z49b+uO2vLGzdqBzfKSY/ynmfLRcxfBYmk99OIT3k=\",\"Nonce\":\"9N3pLnqsXgyh3H7wGXrMQQ==\",\"Verifier\":\"b1z9Rhd8pYSUEczdPx4wWyQTtB9oQFjNzx5EfOfqdNw=\"}"; + private static String getLogins ="{\"RequestType\":\"get-logins\",\"SortSelection\":\"true\",\"TriggerUnlock\":\"false\",\"Id\":\"1234\",\"Nonce\":\"v/93UGpWJYaglsrWTFsq6A==\",\"Verifier\":\"kltoi/9kt2rrv5SReMDYN7zX1XDeBdCPvU78m0qWqVQ=\",\"Url\":\"RNTCyUHMs3smRf99uQx0j/Y0T2dCDAEmYJyx1Gt9zhs=\",\"SubmitUrl\":\"RNTCyUHMs3smRf99uQx0jxIm9b/18UmkIR70L37lxckUpUNjh8Oxre5P6yc74d93OHG3f3OrYEpsItig+IXOAQ==\"}"; + private Gson gson = new GsonBuilder().disableHtmlEscaping().create(); + + + @Test + public void testVerifyAssociateRequestResponse() { + Message.Request r = gson.fromJson(requestString, Message.Request.class); + Processor processor = new Processor(r.Key); + assertTrue(processor.verify(r)); + + Message.Response response = new Message.Response(r.RequestType,""); + processor.makeVerifiable(response); + + assertTrue(processor.verify(response)); + + } + @Test + public void testGetLogins() { + Message.Request r = gson.fromJson(getLoginsAssociate, Message.Request.class); + Processor processor = new Processor(r.Key); + processor.verify(r); + + Message.Request l = gson.fromJson(getLogins, Message.Request.class); + assertTrue(processor.verify(l)); + String encodedUrl = l.Url; + PaddedBufferedBlockCipher cipher = processor.getCipher(Http.CMode.DECRYPT, Helpers.decodeBase64Content(l.Nonce.getBytes(), false)); + String unencodedUrl = processor.CryptoTransform(encodedUrl, true, false, cipher); + System.out.println(unencodedUrl); + assertEquals("https://www.facebook.com", unencodedUrl); + + cipher.reset(); + String encodedSubmitUrl = l.SubmitUrl; + String unencodedSubmitUrl = processor.CryptoTransform(encodedSubmitUrl,true,false, cipher); + System.out.println(unencodedSubmitUrl); + assertEquals("https://www.facebook.com/login.php?login_attempt=1&lwv=110", unencodedSubmitUrl); + + Message.Response response = new Message.Response(l.RequestType, processor.getHash()); + response.Success=true; + response.Count=1; + response.Entries.add(new Message.ResponseEntry("a", "b", "c","uuid", new ArrayList())); + processor.makeVerifiable(response); + + assertTrue(processor.verify(response)); + } + + +} diff --git a/http/src/test/java/trace.txt b/http/src/test/java/trace.txt new file mode 100644 index 00000000..3833b0ef --- /dev/null +++ b/http/src/test/java/trace.txt @@ -0,0 +1,228 @@ +18:26:34.769 [main] INFO proxy - Awaiting connection +18:26:51.472 [Connection 0 ->] INFO proxy - upstream starting +18:26:51.472 [Connection 0 <-] INFO proxy - downstream starting + + + +18:26:51.473 [Connection 0 ->] INFO proxy - POST / HTTP/1.1 +Host: localhost:19455 +Connection: keep-alive +Content-Length: 54 +User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36 +Origin: chrome-extension://ompiailgknfdndiefoaoiligalphfdae +Content-Type: application/json +Accept: */* +Accept-Encoding: gzip, deflate, br +Accept-Language: en-US,en;q=0.8 + +{"RequestType":"test-associate","TriggerUnlock":false} + + + +18:26:51.541 [Connection 0 <-] INFO proxy - HTTP/1.1 200 OK +Content-Length: 128 +Content-Type: application/json +Server: Microsoft-HTTPAPI/2.0 +Date: Mon, 31 Oct 2016 18:26:51 GMT + +{"RequestType":"test-associate","Success":false,"Count":0,"Version":"1.8.4.1","Hash":"eefc4faf792a59d034da69c69f3643ef34ba7d8d"} + + + +18:26:57.523 [Connection 0 ->] INFO proxy - POST / HTTP/1.1 +Host: localhost:19455 +Connection: keep-alive +Content-Length: 54 +User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36 +Origin: chrome-extension://ompiailgknfdndiefoaoiligalphfdae +Content-Type: application/json +Accept: */* +Accept-Encoding: gzip, deflate, br +Accept-Language: en-US,en;q=0.8 + +{"RequestType":"test-associate","TriggerUnlock":false} + + + +18:26:57.589 [Connection 0 <-] INFO proxy - HTTP/1.1 200 OK +Content-Length: 128 +Content-Type: application/json +Server: Microsoft-HTTPAPI/2.0 +Date: Mon, 31 Oct 2016 18:26:57 GMT + +{"RequestType":"test-associate","Success":false,"Count":0,"Version":"1.8.4.1","Hash":"eefc4faf792a59d034da69c69f3643ef34ba7d8d"} + + + +18:26:57.594 [Connection 0 ->] INFO proxy - POST / HTTP/1.1 +Host: localhost:19455 +Connection: keep-alive +Content-Length: 173 +User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36 +Origin: chrome-extension://ompiailgknfdndiefoaoiligalphfdae +Content-Type: application/json +Accept: */* +Accept-Encoding: gzip, deflate, br +Accept-Language: en-US,en;q=0.8 + +{"RequestType":"associate","Key":"NTxz7Rej/a4H9OFXBmXxMp0BjIfqhy5QEYjLnqhWdkA=","Nonce":"UJUjvviWfVDK7c6KzZNUkw==","Verifier":"f3DwAdI7rcWAH0DzGlCMTjLtH2q2+eBLNgRuojQWLS8="} + + + +18:27:28.276 [Connection 0 <-] INFO proxy - HTTP/1.1 200 OK +Content-Length: 236 +Content-Type: application/json +Server: Microsoft-HTTPAPI/2.0 +Date: Mon, 31 Oct 2016 18:27:28 GMT + +{"RequestType":"associate","Success":true,"Id":"NEW coNECTION","Count":0,"Version":"1.8.4.1","Hash":"eefc4faf792a59d034da69c69f3643ef34ba7d8d","Nonce":"/cdtpNsn3sjiwVtrwYmx3g==","Verifier":"5onGXGq18yT4BM3QJnqUxXMKJ7/4fnRvjZ6R0OO0RSE="} + + + +18:27:49.553 [Connection 0 ->] INFO proxy - POST / HTTP/1.1 +Host: localhost:19455 +Connection: keep-alive +Content-Length: 54 +User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36 +Origin: chrome-extension://ompiailgknfdndiefoaoiligalphfdae +Content-Type: application/json +Accept: */* +Accept-Encoding: gzip, deflate, br +Accept-Language: en-US,en;q=0.8 + +{"RequestType":"test-associate","TriggerUnlock":false} + + + +18:27:49.619 [Connection 0 <-] INFO proxy - HTTP/1.1 200 OK +Content-Length: 128 +Content-Type: application/json +Server: Microsoft-HTTPAPI/2.0 +Date: Mon, 31 Oct 2016 18:27:49 GMT + +{"RequestType":"test-associate","Success":false,"Count":0,"Version":"1.8.4.1","Hash":"eefc4faf792a59d034da69c69f3643ef34ba7d8d"} + + + +18:29:38.604 [Connection 0 ->] INFO proxy - POST / HTTP/1.1 +Host: localhost:19455 +Connection: keep-alive +Content-Length: 54 +User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36 +Origin: chrome-extension://ompiailgknfdndiefoaoiligalphfdae +Content-Type: application/json +Accept: */* +Accept-Encoding: gzip, deflate, br +Accept-Language: en-US,en;q=0.8 + +{"RequestType":"test-associate","TriggerUnlock":false} + + + +18:29:38.663 [Connection 0 <-] INFO proxy - HTTP/1.1 200 OK +Content-Length: 128 +Content-Type: application/json +Server: Microsoft-HTTPAPI/2.0 +Date: Mon, 31 Oct 2016 18:29:38 GMT + +{"RequestType":"test-associate","Success":false,"Count":0,"Version":"1.8.4.1","Hash":"eefc4faf792a59d034da69c69f3643ef34ba7d8d"} + + + +18:29:38.670 [Connection 0 ->] INFO proxy - POST / HTTP/1.1 +Host: localhost:19455 +Connection: keep-alive +Content-Length: 345 +User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36 +Origin: chrome-extension://ompiailgknfdndiefoaoiligalphfdae +Content-Type: application/json +Accept: */* +Accept-Encoding: gzip, deflate, br +Accept-Language: en-US,en;q=0.8 + +{"RequestType":"get-logins","SortSelection":"true","TriggerUnlock":"false","Id":"NEW coNECTION","Nonce":"0zKxhTxwUiGfPYtu0WPrhQ==","Verifier":"7hOpUKLJUyFeIdZiBBUFTwV9JzrdxMSELdeaoXt2ias=","Url":"thIyzORoaXem7BvHrrG+BCEOZ9B2hrWFbyTUEE+Ua6o=","SubmitUrl":"thIyzORoaXem7BvHrrG+BIyLvjut2ACp3PmMgnvDzI3vh+pEe4XyKg9R+Oum1q1b62z+9mrXwpOKtnR/g9ogTQ=="} + + + +18:29:38.758 [Connection 0 <-] INFO proxy - HTTP/1.1 200 OK +Content-Length: 250 +Content-Type: application/json +Server: Microsoft-HTTPAPI/2.0 +Date: Mon, 31 Oct 2016 18:29:38 GMT + +{"RequestType":"get-logins","Success":true,"Id":"NEW coNECTION","Count":0,"Version":"1.8.4.1","Hash":"eefc4faf792a59d034da69c69f3643ef34ba7d8d","Nonce":"NARnSmq7mlnaqNuZLpE3WQ==","Verifier":"Sq4ayYZIz86EO6MSjflPfsP7g7jWg85moHnwqA8V0do=","Entries":[]} + + + +18:34:31.067 [Connection 0 ->] INFO proxy - POST / HTTP/1.1 +Host: localhost:19455 +Connection: keep-alive +Content-Length: 54 +User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36 +Origin: chrome-extension://ompiailgknfdndiefoaoiligalphfdae +Content-Type: application/json +Accept: */* +Accept-Encoding: gzip, deflate, br +Accept-Language: en-US,en;q=0.8 + +{"RequestType":"test-associate","TriggerUnlock":false} + + + +18:34:31.131 [Connection 0 <-] INFO proxy - HTTP/1.1 200 OK +Content-Length: 128 +Content-Type: application/json +Server: Microsoft-HTTPAPI/2.0 +Date: Mon, 31 Oct 2016 18:34:31 GMT + +{"RequestType":"test-associate","Success":false,"Count":0,"Version":"1.8.4.1","Hash":"eefc4faf792a59d034da69c69f3643ef34ba7d8d"} + + + +18:34:31.135 [Connection 0 ->] INFO proxy - POST / HTTP/1.1 +Host: localhost:19455 +Connection: keep-alive +Content-Length: 168 +User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36 +Origin: chrome-extension://ompiailgknfdndiefoaoiligalphfdae +Content-Type: application/json +Accept: */* +Accept-Encoding: gzip, deflate, br +Accept-Language: en-US,en;q=0.8 + +{"RequestType":"test-associate","TriggerUnlock":false,"Id":"NEW coNECTION","Nonce":"gWgoWCbp6/9YsuvVH3hSPw==","Verifier":"NQ0d51+hW4Y7NBB0Ns6QKPg0BSWnHwHzleMKpZsdO54="} + + + +18:34:31.193 [Connection 0 <-] INFO proxy - HTTP/1.1 200 OK +Content-Length: 241 +Content-Type: application/json +Server: Microsoft-HTTPAPI/2.0 +Date: Mon, 31 Oct 2016 18:34:31 GMT + +{"RequestType":"test-associate","Success":true,"Id":"NEW coNECTION","Count":0,"Version":"1.8.4.1","Hash":"eefc4faf792a59d034da69c69f3643ef34ba7d8d","Nonce":"HMtkcQjmeRxxm8/GLcPsAQ==","Verifier":"t+wp+yIoG497STtDUf7st9yGSLStHglihAO+GMFHnzg="} + + + +18:34:31.207 [Connection 0 ->] INFO proxy - POST / HTTP/1.1 +Host: localhost:19455 +Connection: keep-alive +Content-Length: 345 +User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36 +Origin: chrome-extension://ompiailgknfdndiefoaoiligalphfdae +Content-Type: application/json +Accept: */* +Accept-Encoding: gzip, deflate, br +Accept-Language: en-US,en;q=0.8 + +{"RequestType":"get-logins","SortSelection":"true","TriggerUnlock":"false","Id":"NEW coNECTION","Nonce":"79y7EyDVndGn3B5iFLp4Kw==","Verifier":"LIY9XDHscI+xgrzIVCFnqsW9yigiNzWbJ1ZpOIBKGVs=","Url":"eZpW5dUli05H5l1+MuwiUNjXZyNemZDmQOFjgkz8uvg=","SubmitUrl":"eZpW5dUli05H5l1+MuwiUO2/H/Faw6yvjvgrCElBRSSbGRJ3a4hXFdq0ws75fK26XT9SQQXqwOvCiTiK5Ybnxg=="} + + + +18:34:52.694 [Connection 0 <-] INFO proxy - HTTP/1.1 200 OK +Content-Length: 432 +Content-Type: application/json +Server: Microsoft-HTTPAPI/2.0 +Date: Mon, 31 Oct 2016 18:34:52 GMT + +{"RequestType":"get-logins","Success":true,"Id":"NEW coNECTION","Count":1,"Version":"1.8.4.1","Hash":"eefc4faf792a59d034da69c69f3643ef34ba7d8d","Nonce":"hQ2JSllJLrVWv1LcxN8+rQ==","Verifier":"TT2014GvCXk+bruePydwqiGFeupP+xv7F7UnC5lc7vo=","Entries":[{"Login":"n1qi36EQkTQhzQiT7TFonA==","Password":"gvgmfbelEa3QxvzRsEG0Hg==","Uuid":"w5MJa4za3DLh+2CmbG9Yl5kr5Gba0wnptN24kZPsWHPA8hPx6Pb1XYxiseh5gARY","Name":"xpCqAwBjmUOqVyI6dfJqGA=="}]} diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java index e6c6854a..468662d6 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java @@ -19,7 +19,6 @@ import com.google.common.io.ByteStreams; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Hex; -import org.jetbrains.annotations.Nullable; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -89,8 +88,11 @@ public static String fromDate(Date value) { return inFormat.format(value); } - @Nullable - public static byte[] decodeBase64Content(byte[] content, boolean isCompressed) { + public static byte[] decodeBase64Content(byte[] content) { + return decodeBase64Content(content, false); + } + + public static byte[] decodeBase64Content (byte[] content, boolean isCompressed) { byte[] value = Base64.decodeBase64(content); if (isCompressed) { return unzipBinaryContent(value); @@ -98,7 +100,6 @@ public static byte[] decodeBase64Content(byte[] content, boolean isCompressed) { return value; } - @Nullable public static byte[] unzipBinaryContent(byte[] content) { ByteArrayInputStream bais = new ByteArrayInputStream(content); try { @@ -109,6 +110,10 @@ public static byte[] unzipBinaryContent(byte[] content) { } } + public static String encodeBase64Content(byte[] value) { + return encodeBase64Content(value); + } + public static String encodeBase64Content(byte[] value, boolean isCompressed) { if (!isCompressed) { return Base64.encodeBase64String(value); diff --git a/pom.xml b/pom.xml index 9f495a1d..4bfd79b0 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,7 @@ jaxb all example + http pom From 3146221e6b0b0e64d2f64856d90df32c3513bf0c Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 21 Nov 2016 09:29:04 +0000 Subject: [PATCH 002/228] Typo plus tidy --- .../pwdb/kdbx/stream_3_1/KdbxSerializer.java | 2 +- .../pwdb/checks/BasicDatabaseChecks.java | 36 +++++++++---------- .../pwdb/checks/DatabaseLoaderChecks.java | 17 ++++----- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.java index 108fe1cb..c2477012 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.java @@ -126,7 +126,7 @@ private static void checkStartBytes(KdbxHeader kdbxHeader, InputStream decrypted byte [] startBytes = new byte[32]; ledis.readFully(startBytes); if (!Arrays.equals(startBytes, kdbxHeader.getStreamStartBytes())) { - throw new IllegalStateException("Inconsistent stream start bytes. This usually means the credentials were wromng."); + throw new IllegalStateException("Inconsistent stream start bytes. This usually means the credentials were wrong."); } } diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java index 9c7fb462..4179bc41 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java @@ -32,11 +32,11 @@ /** * @author Jo */ -public abstract class BasicDatabaseChecks { +public abstract class BasicDatabaseChecks , G extends Group, E extends Entry, I extends Icon> { - private Database database; + private Database database; - public abstract Database createDatabase() throws IOException; + public abstract Database createDatabase() throws IOException; public BasicDatabaseChecks() throws IOException { this.database = createDatabase(); @@ -73,10 +73,10 @@ public void testAddGroup() { @Test public void testDeleteGroup () { - Group g1 = database.getRootGroup().addGroup(database.newGroup("group1")); - List l1 = database.getRootGroup().findGroups("group1"); + Group g1 = database.getRootGroup().addGroup(database.newGroup("group1")); + List l1 = database.getRootGroup().findGroups("group1"); Assert.assertTrue(l1.size() == 1); - Group g2 = l1.get(0); + G g2 = l1.get(0); Assert.assertTrue (g2.equals(g1)); Group g3 = database.getRootGroup().removeGroup(g2); Assert.assertTrue (g3.equals(g1)); @@ -87,13 +87,13 @@ public void testDeleteGroup () { @Test public void testAddRemoveEntry() { - Entry e1 = database.getRootGroup().addEntry(database.newEntry()); + E e1 = database.getRootGroup().addEntry(database.newEntry()); e1.setTitle("entry1"); - List l1 = database.findEntries("entry1"); + List l1 = database.findEntries("entry1"); Assert.assertTrue(l1.size() == 1); - Entry e12 = database.getRootGroup().addEntry(database.newEntry("entry12")); - List l2 = database.findEntries("entry1"); + E e12 = database.getRootGroup().addEntry(database.newEntry("entry12")); + List l2 = database.findEntries("entry1"); Assert.assertTrue(l2.size() == 2); // show that the entries are different @@ -113,7 +113,7 @@ public void testAddRemoveEntry() { @Test public void testSetFields () { - Entry e1 = database.newEntry("Entry 1"); + E e1 = database.newEntry("Entry 1"); e1.setNotes("this looks a little like Entry 2"); Assert.assertTrue(e1.getNotes().equals("this looks a little like Entry 2")); e1.setUsername("jake@window.com"); @@ -128,7 +128,7 @@ public void testSetFields () { Assert.assertTrue(e1.matchTitle("1")); Assert.assertFalse(e1.matchTitle("doggy")); - Icon ic1 = database.newIcon(27); + I ic1 = database.newIcon(27); e1.setIcon(ic1); Assert.assertTrue(e1.getIcon().equals(ic1)); @@ -166,7 +166,7 @@ public void testSetFields () { @Test public void testNewEntry() { - Entry e2 = database.newEntry(); + E e2 = database.newEntry(); Assert.assertNull(e2.getParent()); assertEquals("", e2.getPassword()); Assert.assertNotNull(e2.getUuid()); @@ -182,7 +182,7 @@ public void testNewEntry() { @Test public void testCopy() throws IOException { - Entry entry1 = database.newEntry(); + E entry1 = database.newEntry(); entry1.setTitle("Entry"); entry1.setUsername("Username"); entry1.setPassword("Password"); @@ -191,9 +191,9 @@ public void testCopy() throws IOException { entry1.setIcon(database.newIcon(2)); // create a new Database - Database database2 = createDatabase(); + Database database2 = createDatabase(); // create a new Entry in new Database - Entry entry2 = database2.newEntry(entry1); + E entry2 = database2.newEntry(entry1); assertEquals(entry1.getTitle(), entry2.getTitle()); assertEquals(entry1.getUsername(), entry2.getUsername()); @@ -203,11 +203,11 @@ public void testCopy() throws IOException { assertEquals(entry1.getIcon(), entry2.getIcon()); assertNotEquals(entry1.getUuid(), entry2.getUuid()); - Group group1 = database.newGroup(); + G group1 = database.newGroup(); group1.setName("Group"); group1.setIcon(database.newIcon(3)); - Group group2 = database2.newGroup(group1); + G group2 = database2.newGroup(group1); assertEquals(group1.getName(), group2.getName()); assertEquals(group1.getIcon(), group2.getIcon()); assertNotEquals(group1.getUuid(), group2.getUuid()); diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java index ebddca74..da17cee9 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java @@ -18,20 +18,17 @@ import org.junit.Assert; import org.junit.Test; -import org.linguafranca.pwdb.Database; -import org.linguafranca.pwdb.Entry; -import org.linguafranca.pwdb.Visitor; +import org.linguafranca.pwdb.*; import java.util.List; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; /** * @author jo */ -public abstract class DatabaseLoaderChecks { - protected Database database; +public abstract class DatabaseLoaderChecks , G extends Group, E extends Entry, I extends Icon>{ + protected Database database; /** * a test123 file for each format. Should contain the same thing. This is a basic sanity check. @@ -44,11 +41,11 @@ public void test123File() { // find all entries in the database // the kdb version has three additional system related entries - List anything = database.findEntries(""); + List anything = database.findEntries(""); Assert.assertTrue(10 <= anything.size()); // find all entries in the database that have the string "test" in them - List tests = database.findEntries("test"); + List tests = database.findEntries("test"); for (Entry tes: tests) { System.out.println(tes.getTitle()); } @@ -67,14 +64,14 @@ public void test123File() { Assert.assertEquals("123", pass2); } - List passwords = database.findEntries("password"); + List passwords = database.findEntries("password"); Assert.assertEquals(4, passwords.size()); for (Entry passwordEntry : passwords) { assertEquals(passwordEntry.getTitle(), passwordEntry.getPassword()); System.out.println(passwordEntry.getTitle()); } - List entries = database.findEntries(new Entry.Matcher() { + List entries = database.findEntries(new Entry.Matcher() { @Override public boolean matches(Entry entry) { return entry.getTitle().equals("hello world"); From e43bedb3fbc1a074ef2be7ceb1628270d1b65b9f Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 23 Jan 2017 10:47:03 +0000 Subject: [PATCH 003/228] Further development --- http/pom.xml | 5 + .../linguafranca/pwdb/keepasshttp/Crypto.java | 126 +++++ .../pwdb/keepasshttp/DatabaseAdaptor.java | 48 ++ .../linguafranca/pwdb/keepasshttp/Http.java | 446 ------------------ .../keepasshttp/KeePassJavaHttpHandler.java | 12 + .../keepasshttp/KeePassJavaJettyServer.java | 4 +- .../pwdb/keepasshttp/Processor.java | 159 +------ http/src/test/java/MonitoredRequestsTest.java | 22 +- http/src/test/java/TestRequestResponse.java | 22 +- 9 files changed, 239 insertions(+), 605 deletions(-) create mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java create mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java delete mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/Http.java diff --git a/http/pom.xml b/http/pom.xml index 96f36170..0b99150e 100644 --- a/http/pom.xml +++ b/http/pom.xml @@ -53,6 +53,11 @@ 4.12 test + + org.linguafranca.pwdb + KeePassJava2-simple + 2.1.1-SNAPSHOT + diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java new file mode 100644 index 00000000..c047795e --- /dev/null +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java @@ -0,0 +1,126 @@ +package org.linguafranca.pwdb.keepasshttp; + +import org.linguafranca.pwdb.kdbx.Helpers; +import org.spongycastle.crypto.InvalidCipherTextException; +import org.spongycastle.crypto.engines.AESFastEngine; +import org.spongycastle.crypto.modes.CBCBlockCipher; +import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.spongycastle.crypto.params.KeyParameter; +import org.spongycastle.crypto.params.ParametersWithIV; + +import java.security.SecureRandom; +import java.util.Arrays; + +/** + * @author jo + */ +public class Crypto { + + private byte[] binaryKey; + public byte[] getKey() { + return binaryKey; + } + public void setKey(byte[] binaryKey) { + this.binaryKey = binaryKey; + } + + public enum CMode { + ENCRYPT(true), + DECRYPT(false); + + private final boolean encrypt; + + CMode(boolean encrypt) { + this.encrypt = encrypt; + } + + public boolean getEncrypt() { + return encrypt; + } + } + + /** TODO this can be simplified using the helpers below (it's what they are for) */ + public boolean verify(Message.Verifiable verifiable) { + if (getKey() == null || verifiable.Verifier == null || verifiable.Nonce == null) { + return false; + } + byte[] verifier = Helpers.decodeBase64Content(verifiable.Verifier.getBytes(), false); + byte[] iv = Helpers.decodeBase64Content(verifiable.Nonce.getBytes(), false); + + PaddedBufferedBlockCipher cipher = getCipher(Crypto.CMode.DECRYPT, iv); + + byte[] output = new byte[cipher.getOutputSize(verifier.length)]; + int outputlen = cipher.processBytes(verifier, 0, verifier.length, output, 0); + + try { + cipher.doFinal(output, outputlen); + byte[] comparison = new byte[output.length]; + System.arraycopy(verifiable.Nonce.getBytes(),0,comparison,0,verifiable.Nonce.length()); + return Arrays.equals(output, comparison); + } catch (InvalidCipherTextException e) { + return false; + } + } + + public void makeVerifiable(Message.Response response) { + // we don't have a key? we can't do anything + if (getKey() == null) { + return; + } + + // The nonce is base64 encoded version of an iv + // The verifier is the base64 encoded encrypted version of the nonce + byte[] iv = new SecureRandom().generateSeed(16); + response.Nonce = Helpers.encodeBase64Content(iv, false); + response.Verifier = encryptToBase64(response.Nonce, iv); +/* + PaddedBufferedBlockCipher cipher = getCipher(Crypto.CMode.ENCRYPT, iv); + response.Verifier = CryptoTransform(response.Nonce, false, true, cipher); +*/ + + for (Message.ResponseEntry entry: response.Entries) { + entry.Login = encryptToBase64(entry.Login, iv); + entry.Uuid = encryptToBase64(entry.Uuid, iv); + entry.Name = encryptToBase64(entry.Name, iv); + entry.Password = encryptToBase64(entry.Password, iv); + } + } + + + public PaddedBufferedBlockCipher getCipher(CMode mode, byte[] iv) { + PaddedBufferedBlockCipher result = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); + result.init(mode.getEncrypt(), new ParametersWithIV(new KeyParameter(getKey()), iv)); + return result; + } + + public String decryptFromBase64(String input, byte[] iv){ + return CryptoTransform(input, true, false, getCipher(CMode.DECRYPT, iv)); + } + + public String encryptToBase64(String input, byte[] iv){ + return CryptoTransform(input, false, true, getCipher(CMode.ENCRYPT, iv)); + } + + public static String CryptoTransform(String input, boolean base64in, boolean base64out, PaddedBufferedBlockCipher cipher) { + byte[] bytes; + if (base64in) { + bytes = Helpers.decodeBase64Content(input.getBytes(), false); + } else { + bytes = input.getBytes(); + } + + byte[] output = new byte[cipher.getOutputSize(bytes.length)]; + int outputlen = cipher.processBytes(bytes, 0, bytes.length, output, 0); + try { + int len = cipher.doFinal(output, outputlen); + // padded buffer is required on bas64 i.e. encrypted direction + if (base64out) { + return Helpers.encodeBase64Content(output, false); + } + // trim to buffer length + return new String(output, 0, outputlen + len); + } catch (InvalidCipherTextException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java new file mode 100644 index 00000000..70a03286 --- /dev/null +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java @@ -0,0 +1,48 @@ +package org.linguafranca.pwdb.keepasshttp; + +import org.apache.commons.codec.binary.Hex; +import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.kdbx.Helpers; +import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; +import org.spongycastle.crypto.digests.SHA1Digest; + + +import java.util.UUID; + +/** + * @author jo + */ +public interface DatabaseAdaptor { + + String getId(); + String getHash(); + Database getDatabase(); + + class Default implements DatabaseAdaptor { + private String id = "402881E9-58B6-5A30-0158-B65A30B20000"; + private UUID rootGroupUuid = UUID.fromString("402881E9-58B6-5A30-0158-B65AFC580001"); + private UUID recycleBinUuid = UUID.fromString("402881E9-58B6-5A30-0158-B65BC8D30002"); + private Database database = new SimpleDatabase(); + + @Override + public String getId() { + return id; + } + + @Override + public String getHash() { + byte[] toHash = (Helpers.hexStringFromUuid(rootGroupUuid) + Helpers.hexStringFromUuid(recycleBinUuid)).getBytes(); + SHA1Digest digest = new SHA1Digest(); + byte[] digestBytes = new byte[digest.getDigestSize()]; + digest.update(toHash, 0, toHash.length); + digest.doFinal(digestBytes, 0); + String result = new String(Hex.encodeHex(digestBytes)); + return result.toLowerCase(); + } + + @Override + public Database getDatabase() { + return database; + } + } +} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Http.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Http.java deleted file mode 100644 index 03d1de71..00000000 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Http.java +++ /dev/null @@ -1,446 +0,0 @@ -package org.linguafranca.pwdb.keepasshttp; - -/** - * @author jo - */ -public class Http { - public enum CMode { - ENCRYPT(true), DECRYPT(false); - private final boolean encrypt; - - CMode(boolean encrypt) { - this.encrypt = encrypt; - } - - public boolean getEncrypt() { - return encrypt; - } - } - -/* - public static class KeePassHttpExt - { - - /// - /// an arbitrarily generated uuid for the keepasshttp root entry - /// - public final byte[] KEEPASSHTTP_UUID = { - 0x34, (byte) 0x69, (byte) 0x7a, (byte) 0x40, (byte) 0x8a, (byte) 0x5b, (byte) 0x41, (byte) 0xc0, - (byte) 0x9f, (byte) 0x36, (byte) 0x89, (byte) 0x7d, (byte) 0x62, (byte) 0x3e, (byte) 0xcb, (byte) 0x31 - }; - - private final int DEFAULT_NOTIFICATION_TIME = 5000; - public final String KEEPASSHTTP_NAME = "KeePassHttp Settings"; - private final String KEEPASSHTTP_GROUP_NAME = "KeePassHttp Passwords"; - public final String ASSOCIATE_KEY_PREFIX = "AES Key: "; - private IPluginHost host; - private HttpListener listener; - public final int DEFAULT_PORT = 19455; - public final String DEFAULT_HOST = "localhost"; - /// - /// TODO make configurable - /// - private const String HTTP_SCHEME = "http://"; - //private const String HTTPS_PREFIX = "https://localhost:"; - //private int HTTPS_PORT = DEFAULT_PORT + 1; - private Thread httpThread; - private volatile boolean stopped = false; - Dictionary handlers = new Dictionary(); - - //public String UpdateUrl = ""; - public String UpdateUrl = "https://passifox.appspot.com/kph/latest-version.txt"; - - private SearchParameters MakeSearchParameters() - { - SearchParameters p = new SearchParameters(); - p.SearchInTitles = true; - p.RegularExpression = true; - p.SearchInGroupNames = false; - p.SearchInNotes = false; - p.SearchInOther = false; - p.SearchInPasswords = false; - p.SearchInTags = false; - p.SearchInUrls = true; - p.SearchInUserNames = false; - p.SearchInUuids = false; - return p; - } - - private String CryptoTransform(String input, bool base64in, bool base64out, Aes cipher, CMode mode) - { - byte[] bytes; - if (base64in) - bytes = decode64(input); - else - bytes = Encoding.UTF8.GetBytes(input); - - - using (var c = mode == CMode.ENCRYPT ? cipher.CreateEncryptor() : cipher.CreateDecryptor()) { - var buf = c.TransformFinalBlock(bytes, 0, bytes.Length); - return base64out ? encode64(buf) : Encoding.UTF8.GetString(buf); - } - } - - private PwEntry GetConfigEntry(bool create) - { - var root = host.Database.RootGroup; - var uuid = new PwUuid(KEEPASSHTTP_UUID); - var entry = root.FindEntry(uuid, false); - if (entry == null && create) - { - entry = new PwEntry(false, true); - entry.Uuid = uuid; - entry.Strings.Set(PwDefs.TitleField, new ProtectedString(false, KEEPASSHTTP_NAME)); - root.AddEntry(entry, true); - UpdateUI(null); - } - return entry; - } - - private int GetNotificationTime() - { - var time = DEFAULT_NOTIFICATION_TIME; - var entry = GetConfigEntry(false); - if (entry != null) - { - var s = entry.Strings.ReadSafe("Prompt Timeout"); - if (s != null && s.Trim() != "") - { - try - { - time = Int32.Parse(s) * 1000; - } - catch { } - } - } - - return time; - } - - private void ShowNotification(String text) - { - ShowNotification(text, null, null); - } - - private void ShowNotification(String text, EventHandler onclick) - { - ShowNotification(text, onclick, null); - } - - private void ShowNotification(String text, EventHandler onclick, EventHandler onclose) - { - MethodInvoker m = delegate - { - var notify = host.MainWindow.MainNotifyIcon; - if (notify == null) - return; - - EventHandler clicked = null; - EventHandler closed = null; - - clicked = delegate - { - notify.BalloonTipClicked -= clicked; - notify.BalloonTipClosed -= closed; - if (onclick != null) - onclick(notify, null); - }; - closed = delegate - { - notify.BalloonTipClicked -= clicked; - notify.BalloonTipClosed -= closed; - if (onclose != null) - onclose(notify, null); - }; - - //notify.BalloonTipIcon = ToolTipIcon.Info; - notify.BalloonTipTitle = "KeePassHttp"; - notify.BalloonTipText = text; - notify.ShowBalloonTip(GetNotificationTime()); - // need to add listeners after showing, or closed is sent right away - notify.BalloonTipClosed += closed; - notify.BalloonTipClicked += clicked; - }; - if (host.MainWindow.InvokeRequired) - host.MainWindow.Invoke(m); - else - m.Invoke(); - } - - public override bool Initialize(IPluginHost host) - { - var httpSupported = HttpListener.IsSupported; - this.host = host; - - var optionsMenu = new ToolStripMenuItem("KeePassHttp Options..."); - optionsMenu.Click += OnOptions_Click; - optionsMenu.Image = KeePassHttp.Properties.Resources.earth_lock; - //optionsMenu.Image = global::KeePass.Properties.Resources.B16x16_File_Close; - this.host.MainWindow.ToolsMenu.DropDownItems.Add(optionsMenu); - - if (httpSupported) - { - try - { - handlers.Add(Request.TEST_ASSOCIATE, TestAssociateHandler); - handlers.Add(Request.ASSOCIATE, AssociateHandler); - handlers.Add(Request.GET_LOGINS, GetLoginsHandler); - handlers.Add(Request.GET_LOGINS_COUNT, GetLoginsCountHandler); - handlers.Add(Request.GET_ALL_LOGINS, GetAllLoginsHandler); - handlers.Add(Request.SET_LOGIN, SetLoginHandler); - handlers.Add(Request.GENERATE_PASSWORD, GeneratePassword); - - listener = new HttpListener(); - - var configOpt = new ConfigOpt(this.host.CustomConfig); - - listener.Prefixes.Add(HTTP_SCHEME + configOpt.ListenerHost + ":" + configOpt.ListenerPort.ToString() + "/"); - //listener.Prefixes.Add(HTTPS_PREFIX + HTTPS_PORT + "/"); - listener.Start(); - - httpThread = new Thread(new ThreadStart(Run)); - httpThread.Start(); - } catch (HttpListenerException e) { - MessageBox.Show(host.MainWindow, - "Unable to start HttpListener!\nDo you really have only one installation of KeePassHttp in your KeePass-directory?\n\n" + e, - "Unable to start HttpListener", - MessageBoxButtons.OK, - MessageBoxIcon.Error - ); - } - } - else - { - MessageBox.Show(host.MainWindow, "The .NET HttpListener is not supported on your OS", - ".NET HttpListener not supported", - MessageBoxButtons.OK, - MessageBoxIcon.Error - ); - } - return httpSupported; - } - - void OnOptions_Click(object sender, EventArgs e) - { - var form = new OptionsForm(new ConfigOpt(host.CustomConfig)); - UIUtil.ShowDialogAndDestroy(form); - } - - private void Run() - { - while (!stopped) - { - try - { - var r = listener.BeginGetContext(new AsyncCallback(RequestHandler), listener); - r.AsyncWaitHandle.WaitOne(); - r.AsyncWaitHandle.Close(); - } - catch (ThreadInterruptedException) { } - catch (HttpListenerException e) { - MessageBox.Show(host.MainWindow, "Unable to process request!\n\n" + e, - "Unable to process request", - MessageBoxButtons.OK, - MessageBoxIcon.Error - ); - } - } - } - - private JsonSerializer NewJsonSerializer() - { - var settings = new JsonSerializerSettings(); - settings.DefaultValueHandling = DefaultValueHandling.Ignore; - settings.NullValueHandling = NullValueHandling.Ignore; - - return JsonSerializer.Create(settings); - } - private Response ProcessRequest(Request r, HttpListenerResponse resp) - { - String hash = host.Database.RootGroup.Uuid.ToHexString() + host.Database.RecycleBinUuid.ToHexString(); - hash = getSHA1(hash); - - var response = new Response(r.RequestType, hash); - - using (var aes = new AesManaged()) - { - aes.Mode = CipherMode.CBC; - aes.Padding = PaddingMode.PKCS7; - var handler = handlers[r.RequestType]; - if (handler != null) - { - try - { - handler(r, response, aes); - } - catch (Exception e) - { - ShowNotification("***BUG*** " + e, (s,evt) => MessageBox.Show(host.MainWindow, e + "")); - response.Error = e + ""; - resp.StatusCode = (int)HttpStatusCode.BadRequest; - } - } - else - { - response.Error = "Unknown command: " + r.RequestType; - resp.StatusCode = (int)HttpStatusCode.BadRequest; - } - } - - return response; - } - private void RequestHandler(IAsyncResult r) - { - try { - _RequestHandler(r); - } catch (Exception e) { - MessageBox.Show(host.MainWindow, "RequestHandler failed: " + e); - } - } - private void _RequestHandler(IAsyncResult r) - { - if (stopped) return; - var l = (HttpListener)r.AsyncState; - var ctx = l.EndGetContext(r); - var req = ctx.Request; - var resp = ctx.Response; - - var serializer = NewJsonSerializer(); - Request request = null; - - resp.StatusCode = (int)HttpStatusCode.OK; - using (var ins = new JsonTextReader(new StreamReader(req.InputStream))) - { - try - { - request = serializer.Deserialize(ins); - } - catch (JsonSerializationException e) - { - var buffer = Encoding.UTF8.GetBytes(e + ""); - resp.StatusCode = (int)HttpStatusCode.BadRequest; - resp.ContentLength64 = buffer.Length; - resp.OutputStream.Write(buffer, 0, buffer.Length); - } // ignore, bad request - } - - var db = host.Database; - - var configOpt = new ConfigOpt(this.host.CustomConfig); - - if (request != null && (configOpt.UnlockDatabaseRequest || request.TriggerUnlock == "true") && !db.IsOpen) - { - host.MainWindow.Invoke((MethodInvoker)delegate - { - host.MainWindow.EnsureVisibleForegroundWindow(true, true); - }); - - // UnlockDialog not already opened - bool bNoDialogOpened = (KeePass.UI.GlobalWindowManager.WindowCount == 0); - if (!db.IsOpen && bNoDialogOpened) - { - host.MainWindow.Invoke((MethodInvoker)delegate - { - host.MainWindow.OpenDatabase(host.MainWindow.DocumentManager.ActiveDocument.LockedIoc, null, false); - }); - } - } - - if (request != null && db.IsOpen) - { - Response response = null; - if (request != null) - response = ProcessRequest(request, resp); - - resp.ContentType = "application/json"; - var writer = new StringWriter(); - if (response != null) - { - serializer.Serialize(writer, response); - var buffer = Encoding.UTF8.GetBytes(writer.ToString()); - resp.ContentLength64 = buffer.Length; - resp.OutputStream.Write(buffer, 0, buffer.Length); - } - } - else - { - resp.StatusCode = (int)HttpStatusCode.ServiceUnavailable; - } - - var outs = resp.OutputStream; - outs.Close(); - resp.Close(); - } - - public override void Terminate() - { - stopped = true; - listener.Stop(); - listener.Close(); - httpThread.Interrupt(); - } - - private void UpdateUI(PwGroup group) - { - var win = host.MainWindow; - if (group == null) group = host.Database.RootGroup; - var f = (MethodInvoker) delegate { - win.UpdateUI(false, null, true, group, true, null, true); - }; - if (win.InvokeRequired) - win.Invoke(f); - else - f.Invoke(); - } - - internal String[] GetUserPass(PwEntry entry) - { - return GetUserPass(new PwEntryDatabase(entry, host.Database)); - } - - internal String[] GetUserPass(PwEntryDatabase entryDatabase) - { - // follow references - SprContext ctx = new SprContext(entryDatabase.entry, entryDatabase.database, - SprCompileFlags.All, false, false); - String user = SprEngine.Compile( - entryDatabase.entry.Strings.ReadSafe(PwDefs.UserNameField), ctx); - String pass = SprEngine.Compile( - entryDatabase.entry.Strings.ReadSafe(PwDefs.PasswordField), ctx); - var f = (MethodInvoker)delegate - { - // apparently, SprEngine.Compile might modify the database - host.MainWindow.UpdateUI(false, null, false, null, false, null, false); - }; - if (host.MainWindow.InvokeRequired) - host.MainWindow.Invoke(f); - else - f.Invoke(); - - return new String[] { user, pass }; - } - - /// - /// Liefert den SHA1 Hash - /// - /// EingabeString - /// SHA1 Hash der EingabeStrings - private String getSHA1(String input) - { - //Umwandlung des EingaString in den SHA1 Hash - System.Security.Cryptography.SHA1 sha1 = new System.Security.Cryptography.SHA1CryptoServiceProvider(); - byte[] textToHash = Encoding.Default.GetBytes(input); - byte[] result = sha1.ComputeHash(textToHash); - - //SHA1 Hash in String konvertieren - System.Text.StringBuilder s = new System.Text.StringBuilder(); - foreach (byte b in result) - { - s.Append(b.ToString("x2").ToLower()); - } - - return s.ToString(); - } -} -*/ -} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java index d62a6ff6..6ad5262e 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java @@ -22,6 +22,7 @@ * @author jo */ public class KeePassJavaHttpHandler extends AbstractHandler { + private Logger logger = LoggerFactory.getLogger(KeePassJavaHttpHandler.class); private Gson gson = new GsonBuilder().disableHtmlEscaping().create(); Processor processor = new Processor(); @@ -58,8 +59,19 @@ public void handle(String s, Request request, HttpServletRequest httpServletRequ return; } + if (!processor.getCrypto().verify(request1)) { + httpServletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.Success = false; + response.Error = "Request did not verify"; + processor.getCrypto().makeVerifiable(response); + request.setHandled(true); + return; + } + handler.process(request1, response); + processor.getCrypto().makeVerifiable(response); + OutputStream outputStream = new LogginOutputStream(httpServletResponse.getOutputStream(), logger); Writer writer = new BufferedWriter(new OutputStreamWriter(outputStream)); gson.toJson(response, writer); diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaJettyServer.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaJettyServer.java index 129c7c9d..753130e1 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaJettyServer.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaJettyServer.java @@ -7,12 +7,10 @@ * @author jo */ public class KeePassJavaJettyServer { - public static void main( String[] args ) throws Exception - { + public static void main(String[] args) throws Exception { Server server = new Server(); ServerConnector http = new ServerConnector(server); http.setHost("0.0.0.0"); - //http.setPort(19455); http.setPort(19455); http.setIdleTimeout(300000); server.addConnector(http); diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java index 4227452e..15b98084 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java @@ -1,65 +1,30 @@ package org.linguafranca.pwdb.keepasshttp; -import org.apache.commons.codec.binary.Hex; import org.linguafranca.pwdb.kdbx.Helpers; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.spongycastle.crypto.InvalidCipherTextException; -import org.spongycastle.crypto.digests.SHA1Digest; -import org.spongycastle.crypto.engines.AESFastEngine; -import org.spongycastle.crypto.modes.CBCBlockCipher; -import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; -import org.spongycastle.crypto.params.KeyParameter; -import org.spongycastle.crypto.params.ParametersWithIV; - -import java.security.SecureRandom; -import java.text.SimpleDateFormat; -import java.util.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; /** + * + * Contains message handlers for + * * @author jo */ public class Processor { - private byte[] binaryKey; - private String id; private Map handlers = new HashMap(); + private DatabaseAdaptor adaptor = new DatabaseAdaptor.Default(); + private Crypto crypto = new Crypto(); - private Logger logger = LoggerFactory.getLogger(this.getClass()); - private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - - private String makeId() { - return id = "Today's ID is " + dateFormat.format(new Date()); - } - - private UUID rootGroupUuid = UUID.randomUUID(); - private UUID recycleBinUuid = UUID.randomUUID(); - - public String getHash() { - byte[] toHash = (Helpers.hexStringFromUuid(rootGroupUuid) + Helpers.hexStringFromUuid(recycleBinUuid)).getBytes(); - SHA1Digest digest = new SHA1Digest(); - byte[] digestBytes = new byte[digest.getDigestSize()]; - digest.update(toHash, 0, toHash.length); - digest.doFinal(digestBytes, 0); - String result = new String(Hex.encodeHex(digestBytes)); - return result.toLowerCase(); - } - - public interface RequestHandler { - void process(Message.Request request, Message.Response response); - } - - public RequestHandler getHandler(String requestType) { - return handlers.get(requestType); + public Processor(String base64Key) { + this(Helpers.decodeBase64Content(base64Key.getBytes(), false)); } public Processor(byte[] binaryKey) { this(); - this.binaryKey = binaryKey; - } - - public Processor(String base64Key) { - this(Helpers.decodeBase64Content(base64Key.getBytes())); + crypto.setKey(binaryKey); } public Processor() { @@ -74,126 +39,53 @@ public Processor() { */ } - public PaddedBufferedBlockCipher getCipher(Http.CMode mode, byte[] iv) { - PaddedBufferedBlockCipher result = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); - result.init(mode.getEncrypt(), new ParametersWithIV(new KeyParameter(getKey()), iv)); - return result; - } - - public boolean verify(Message.Verifiable verifiable) { - byte[] verifier = Helpers.decodeBase64Content(verifiable.Verifier.getBytes(), false); - byte[] iv = Helpers.decodeBase64Content(verifiable.Nonce.getBytes(), false); - - PaddedBufferedBlockCipher cipher = getCipher(Http.CMode.DECRYPT, iv); - - byte[] output = new byte[cipher.getOutputSize(verifier.length)]; - int outputlen = cipher.processBytes(verifier, 0, verifier.length, output, 0); - - try { - cipher.doFinal(output, outputlen); - byte[] comparison = new byte[output.length]; - System.arraycopy(verifiable.Nonce.getBytes(),0,comparison,0,verifiable.Nonce.length()); - return Arrays.equals(output, comparison); - } catch (InvalidCipherTextException e) { - return false; - } + public Crypto getCrypto() { + return crypto; } - public void makeVerifiable(Message.Response response) { - byte[] iv = new SecureRandom().generateSeed(16); - response.Nonce = Helpers.encodeBase64Content(iv, false); - PaddedBufferedBlockCipher cipher = getCipher(Http.CMode.ENCRYPT, iv); - response.Verifier = CryptoTransform(response.Nonce, false, true, cipher); - - if (response.RequestType.equals(Message.Type.GET_LOGINS)) { - for (Message.ResponseEntry entry: response.Entries) { - entry.Login = encryptToBase64(entry.Login, response); - entry.Uuid = encryptToBase64(entry.Uuid, response); - entry.Name = encryptToBase64(entry.Name, response); - entry.Password = encryptToBase64(entry.Password, response); - } - } + public String getHash() { + return adaptor.getHash(); } - public String decryptFromBase64(String input, Message.Verifiable verifiable){ - return CryptoTransform(input, true, false, getCipher(Http.CMode.DECRYPT, Helpers.decodeBase64Content(verifiable.Nonce.getBytes()))); + public interface RequestHandler { + void process(Message.Request request, Message.Response response); } - public String encryptToBase64(String input, Message.Verifiable verifiable){ - return CryptoTransform(input, false, true, getCipher(Http.CMode.ENCRYPT, Helpers.decodeBase64Content(verifiable.Nonce.getBytes()))); + public RequestHandler getHandler(String requestType) { + return handlers.get(requestType); } - public String CryptoTransform(String input, boolean base64in, boolean base64out, PaddedBufferedBlockCipher cipher) { - byte[] bytes; - if (base64in) { - bytes = Helpers.decodeBase64Content(input.getBytes(), false); - } else { - bytes = input.getBytes(); - } - - byte[] output = new byte[cipher.getOutputSize(bytes.length)]; - int outputlen = cipher.processBytes(bytes, 0, bytes.length, output, 0); - try { - int len = cipher.doFinal(output, outputlen); - // padded buffer is required on bas64 i.e. encrypted direction - if (base64out) { - return Helpers.encodeBase64Content(output, false); - } - // trim to buffer length - return new String(output, 0, outputlen + len); - } catch (InvalidCipherTextException e) { - throw new IllegalStateException(e); - } - } private class AssociateHandler implements RequestHandler { @Override public void process(Message.Request request, Message.Response response) { - Processor.this.binaryKey = Helpers.decodeBase64Content(request.Key.getBytes()); - if (!verify(request)) { - logger.warn("Verification failed for " + request.RequestType); - return; - } + Processor.this.crypto.setKey(Helpers.decodeBase64Content(request.Key.getBytes(), false)); - response.Id = makeId(); + response.Id = adaptor.getId(); response.Success = true; - makeVerifiable(response); } } private class TestAssociateHandler implements RequestHandler { @Override public void process(Message.Request request, Message.Response response) { - if (request.Verifier == null || request.Nonce == null) { - return; - } - if (!verify(request)) { - logger.warn("Verification failed for " + request.RequestType); - return; - } response.Success = false; if (request.Id != null) { - response.Success = request.Id.equals(id); + response.Success = request.Id.equals(adaptor.getId()); } - makeVerifiable(response); } } private class GetLoginsHandler implements Processor.RequestHandler { public void process(Message.Request r, Message.Response resp) { - if (!verify(r)) { - logger.warn("Verification failed for " + r.RequestType); - return; - } resp.Entries.add(new Message.ResponseEntry("FB", "FBLOGIN", "FBPASS", Helpers.base64FromUuid(UUID.randomUUID()))); resp.Entries.add(new Message.ResponseEntry("FB2", "FBLOGIN2", "FBPASS2", Helpers.base64FromUuid(UUID.randomUUID()))); - resp.Id = id; + resp.Id = adaptor.getId(); resp.Success = true; resp.Count = 0; - makeVerifiable(resp); /* string submithost = null; @@ -354,7 +246,4 @@ public void process(Message.Request r, Message.Response resp) { } } - private byte[] getKey() { - return binaryKey; - } } diff --git a/http/src/test/java/MonitoredRequestsTest.java b/http/src/test/java/MonitoredRequestsTest.java index 83177fc8..2a370834 100644 --- a/http/src/test/java/MonitoredRequestsTest.java +++ b/http/src/test/java/MonitoredRequestsTest.java @@ -1,6 +1,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import org.junit.Test; +import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.keepasshttp.Message; import org.linguafranca.pwdb.keepasshttp.Processor; @@ -34,9 +35,9 @@ public void sessionTest() { computedResponse.Id = "NEW coNECTION"; System.out.println(gson.toJson(computedResponse)); - processor.verify(computedResponse); + processor.getCrypto().verify(computedResponse); System.out.println(gson.toJson(actualResponse)); - processor.verify(actualResponse); + processor.getCrypto().verify(actualResponse); @@ -48,9 +49,9 @@ public void sessionTest() { computedResponse.Id = "NEW coNECTION"; System.out.println(gson.toJson(computedResponse)); - processor.verify(computedResponse); + processor.getCrypto().verify(computedResponse); System.out.println(gson.toJson(actualResponse)); - processor.verify(actualResponse); + processor.getCrypto().verify(actualResponse); request = gson.fromJson(getLoginsRequest, Message.Request.class); @@ -61,14 +62,15 @@ public void sessionTest() { computedResponse.Id = "NEW coNECTION"; System.out.println(gson.toJson(computedResponse)); - processor.verify(computedResponse); + processor.getCrypto().verify(computedResponse); System.out.println(gson.toJson(actualResponse)); - processor.verify(actualResponse); + processor.getCrypto().verify(actualResponse); + byte[] iv = Helpers.decodeBase64Content(actualResponse.Nonce.getBytes(), false); for (Message.ResponseEntry entry: actualResponse.Entries) { - System.out.println(processor.decryptFromBase64(entry.Login, actualResponse)); - System.out.println(processor.decryptFromBase64(entry.Name, actualResponse)); - System.out.println(processor.decryptFromBase64(entry.Password, actualResponse)); - System.out.println(processor.decryptFromBase64(entry.Uuid, actualResponse)); + System.out.println(processor.getCrypto().decryptFromBase64(entry.Login, iv)); + System.out.println(processor.getCrypto().decryptFromBase64(entry.Name, iv)); + System.out.println(processor.getCrypto().decryptFromBase64(entry.Password, iv)); + System.out.println(processor.getCrypto().decryptFromBase64(entry.Uuid, iv)); } diff --git a/http/src/test/java/TestRequestResponse.java b/http/src/test/java/TestRequestResponse.java index 4421c809..8266f94d 100644 --- a/http/src/test/java/TestRequestResponse.java +++ b/http/src/test/java/TestRequestResponse.java @@ -2,7 +2,7 @@ import com.google.gson.GsonBuilder; import org.junit.Test; import org.linguafranca.pwdb.kdbx.Helpers; -import org.linguafranca.pwdb.keepasshttp.Http; +import org.linguafranca.pwdb.keepasshttp.Crypto; import org.linguafranca.pwdb.keepasshttp.Processor; import org.linguafranca.pwdb.keepasshttp.Message; import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; @@ -27,31 +27,31 @@ public class TestRequestResponse { public void testVerifyAssociateRequestResponse() { Message.Request r = gson.fromJson(requestString, Message.Request.class); Processor processor = new Processor(r.Key); - assertTrue(processor.verify(r)); + assertTrue(processor.getCrypto().verify(r)); Message.Response response = new Message.Response(r.RequestType,""); - processor.makeVerifiable(response); + processor.getCrypto().makeVerifiable(response); - assertTrue(processor.verify(response)); + assertTrue(processor.getCrypto().verify(response)); } @Test public void testGetLogins() { Message.Request r = gson.fromJson(getLoginsAssociate, Message.Request.class); Processor processor = new Processor(r.Key); - processor.verify(r); + processor.getCrypto().verify(r); Message.Request l = gson.fromJson(getLogins, Message.Request.class); - assertTrue(processor.verify(l)); + assertTrue(processor.getCrypto().verify(l)); String encodedUrl = l.Url; - PaddedBufferedBlockCipher cipher = processor.getCipher(Http.CMode.DECRYPT, Helpers.decodeBase64Content(l.Nonce.getBytes(), false)); - String unencodedUrl = processor.CryptoTransform(encodedUrl, true, false, cipher); + PaddedBufferedBlockCipher cipher = processor.getCrypto().getCipher(Crypto.CMode.DECRYPT, Helpers.decodeBase64Content(l.Nonce.getBytes(), false)); + String unencodedUrl = processor.getCrypto().CryptoTransform(encodedUrl, true, false, cipher); System.out.println(unencodedUrl); assertEquals("https://www.facebook.com", unencodedUrl); cipher.reset(); String encodedSubmitUrl = l.SubmitUrl; - String unencodedSubmitUrl = processor.CryptoTransform(encodedSubmitUrl,true,false, cipher); + String unencodedSubmitUrl = processor.getCrypto().CryptoTransform(encodedSubmitUrl,true,false, cipher); System.out.println(unencodedSubmitUrl); assertEquals("https://www.facebook.com/login.php?login_attempt=1&lwv=110", unencodedSubmitUrl); @@ -59,9 +59,9 @@ public void testGetLogins() { response.Success=true; response.Count=1; response.Entries.add(new Message.ResponseEntry("a", "b", "c","uuid", new ArrayList())); - processor.makeVerifiable(response); + processor.getCrypto().makeVerifiable(response); - assertTrue(processor.verify(response)); + assertTrue(processor.getCrypto().verify(response)); } From 75d454383afddef92013dcbe61fa3789b6b856f0 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 23 Jan 2017 11:14:02 +0000 Subject: [PATCH 004/228] Add a test to verify fix to DeletedObjects --- .../pwdb/kdbx/simple/SimpleDatabaseLoadTest.java | 7 +++++++ test/src/main/resources/testDeleted.kdbx | Bin 0 -> 1886 bytes 2 files changed, 7 insertions(+) create mode 100755 test/src/main/resources/testDeleted.kdbx diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java index 01e22d02..8e06bbad 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java @@ -46,4 +46,11 @@ public void emptyDb() throws Exception { System.out.println(database.getDescription()); } + @Test + public void dbWithDeleted() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("testDeleted.kdbx"); + SimpleDatabase database = SimpleDatabase.load(new KdbxCreds("123".getBytes()), inputStream); + database.visit(new Visitor.Print()); + } + } \ No newline at end of file diff --git a/test/src/main/resources/testDeleted.kdbx b/test/src/main/resources/testDeleted.kdbx new file mode 100755 index 0000000000000000000000000000000000000000..4a5353c48dd49373a02ad8673a0cbf7804917680 GIT binary patch literal 1886 zcmV-k2ch@_*`k_f`%AR}00RI55CAd3^5(yBLr}h01tDtuTK@wC0096100bZagD3d#JPL6M3=IB+XC zT$7OL-_Tq;OMo*52mo*w00000000LN03d1C?9p~r<<_JDQH#D?$p|0-57qt}?3^Q5 zYo!8EDeSEodpT?D@vGUgZ=2_OL5YFHPyb1|>J-uJqs0rIhF$;3u^{|EF| zv6jYl*}g^!1ONg60000401XNa3Z!BpbY!_9OQ(Z_<^X^L$GZ^CQ+#cIjgA;^kWd2| zL|;M1JorX-iR^b+P?{S1xQ+TM^jR&1Eq$8#bBp186;X4m{VV)kjh;>-tZ_17r^d7H za!bgwt~#FR(paFlWF=Om1vS_9ycWoA|9+2T#Rj4Bijk{wC!O*^mc4A4Yq{1V92fO1 ziVC=(B3MTrfsfjVcDgp%3Y>ZKKzePYfyh0S78ju*XuzIRX3o_3fTO<(Xi zE8HXPV>ej+iQG_Pl{(z62v4%jSg!K5bmc8BODbi)7D)4^3+XDz{>Shm4#0eYKRfl~ zmSkNl#l((_w;cr&ls9FVz!0v7V8+l4OHkgEA2Q*K98y+M@yM!qRsuiWw?6$>AyxJn zbl)Oo5_vQUNTP1o2${z22)PdlDd4(zpcoS6FWa){tP}`>2fn}YRNBZW*%ySx{tP&k zs`+_njZKqVp0T^Dg?0#>tBi!To!6QbUo2vZ~T>4sUR^$FVvbS(X<8>;yYKRnR_pXrOH0Py&%2&|oYvGW zlIb6(NMEyv@KL8=;8Fmg9{|rk;CRLNZs6!0xus9@gplAA_N{}ka5i*6GA_N&p5{* z{Eiy1wErvNOZ`M%6dfN~`cTf{R5V@ziqzlRK~z9X=4q1pM~H3n9hm$qeraHHAyt_G z?Y@~u-b#At*qF>=F+O}=6v8m za*|r*-a=CG3$B9w`Ng)g(?9hws?KqyncrV_l!yNNa@N$kg?}6KY$0;q?cBaT{L^f7Z0aW#XY#NXggJGrU}g%;)n|L_s_NL!Tc(%bN;bt-gBJa^SoNdO$M1f-8W= zyjfuwXM1G$tL%-PCL=)UWbzT{i5@qPr$UjR?RigG8qmBVK7IP$)5jl244u^@Db3|6 z5LeR~wHboS!=$z&vL-j>4u^-sU8u7yMPswb^iQBCk+2lZ*2>F}x2DPNJ?L%5YV!KG zMPa#qe29+juEIVS#{_qn-hzub;Hm-!#hUf6JKsNF0@dV4mwk1 zJa2Q?LTRlk_kJ9lnM9ikDjJ{>Np96fH;c?f$3Pff)k(8!bP+t%b)o0{*}J@Mt#!)% zXu=XWIwARqLTMVj7-OQQc&+ri=DCytf#{it3G3hIL&Y#bocsfkD&d z8l~UI7GT6jMEaA_S0Z*dM1a?XY=X73~U3wll>og@F#Kt Yu4mhHC=Ug$h literal 0 HcmV?d00001 From ef01b2431ee82f1bbd36c5df5783106788671207 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 23 Jan 2017 13:32:16 +0000 Subject: [PATCH 005/228] Add support for kdb key file --- .../linguafranca/pwdb/kdb/KdbCredentials.java | 39 +++++++++++++++++- .../linguafranca/pwdb/kdb/KdbSerializer.java | 2 +- .../linguafranca/pwdb/kdb/KeyFileTest.java | 24 +++++++++++ test/src/main/resources/kdb.key | 1 + test/src/main/resources/kdbwithkey.kdb | Bin 0 -> 1484 bytes 5 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 kdb/src/test/java/org/linguafranca/pwdb/kdb/KeyFileTest.java create mode 100755 test/src/main/resources/kdb.key create mode 100755 test/src/main/resources/kdbwithkey.kdb diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java index 616c331e..b9231ccd 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java @@ -16,9 +16,13 @@ package org.linguafranca.pwdb.kdb; +import com.google.common.io.ByteStreams; import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.security.Encryption; +import org.spongycastle.util.encoders.Hex; +import java.io.IOException; +import java.io.InputStream; import java.security.MessageDigest; /** @@ -31,8 +35,9 @@ public interface KdbCredentials extends Credentials { /** * Password only credentials */ - public static class Password implements KdbCredentials { - private byte [] key; + class Password implements KdbCredentials { + + private final byte [] key; public Password(byte[] password) { MessageDigest md = Encryption.getMessageDigestInstance(); @@ -44,4 +49,34 @@ public byte[] getKey() { return key; } } + + /** + * Key file credentials + */ + class KeyFile implements KdbCredentials { + + private final byte[] key; + + public KeyFile(byte[] password, InputStream inputStream) { + MessageDigest md = Encryption.getMessageDigestInstance(); + byte[] pwKey = md.digest(password); + md.update(pwKey); + + try { + byte [] keyFileData = ByteStreams.toByteArray(inputStream); + if (keyFileData.length == 64) { + keyFileData = Hex.decode(keyFileData); + } + key = md.digest(keyFileData); + } catch (IOException e) { + throw new IllegalStateException("Could not read key file", e); + } + } + + @Override + public byte[] getKey() { + return this.key; + } + + } } diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java index 07c88214..45a211a2 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java @@ -212,7 +212,7 @@ private static KdbGroup deserializeGroup(KdbGroup lastGroup, DataInput dataInput group.setFlags(readInt(dataInput)); break; default: - throw new IllegalStateException("Unknown field type"); + throw new IllegalStateException("Unknown field type " + String.valueOf(fieldType)); } } dataInput.readInt(); diff --git a/kdb/src/test/java/org/linguafranca/pwdb/kdb/KeyFileTest.java b/kdb/src/test/java/org/linguafranca/pwdb/kdb/KeyFileTest.java new file mode 100644 index 00000000..c78ba10a --- /dev/null +++ b/kdb/src/test/java/org/linguafranca/pwdb/kdb/KeyFileTest.java @@ -0,0 +1,24 @@ +package org.linguafranca.pwdb.kdb; + +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.Assert.assertEquals; + +/** + * @author jo + */ +public class KeyFileTest { + + @Test + public void openKdbWithKeyFile() throws IOException { + InputStream key = getClass().getClassLoader().getResourceAsStream("kdb.key"); + KdbCredentials creds = new KdbCredentials.KeyFile("123".getBytes(), key); + InputStream is = getClass().getClassLoader().getResourceAsStream("kdbwithkey.kdb"); + KdbDatabase db = KdbDatabase.load(creds, is); + assertEquals(1, db.getRootGroup().getGroupsCount()); + assertEquals("General", db.getRootGroup().getGroups().get(0).getName()); + } +} \ No newline at end of file diff --git a/test/src/main/resources/kdb.key b/test/src/main/resources/kdb.key new file mode 100755 index 00000000..d2f12aef --- /dev/null +++ b/test/src/main/resources/kdb.key @@ -0,0 +1 @@ +a320d61f69f0903d9f4cecb95c81b35f77b1af232a0b8f2d84377f7df57022d4 \ No newline at end of file diff --git a/test/src/main/resources/kdbwithkey.kdb b/test/src/main/resources/kdbwithkey.kdb new file mode 100755 index 0000000000000000000000000000000000000000..b69e5b702114de3a431d6b1e9cee53f3d1ffc1cb GIT binary patch literal 1484 zcmV;-1vB~s*`k_d`%AS00000400RKY!6tOq`b9B-5qEbI1mnA^D0?uO&G~Vrzsv^D^7a%QL6ORL6U= zwEb<3?w)#G$|XONaF*62uLu;Rdxn!H=Zto9EGYxCiksposIwYiY-hrwb2+!54@`*( zjHfwsSU^7iNU(8;dA$`-Aqjjm4#D}3`A)ZD8$9K@|CYZ>aw9CGvyg`ZN@ z;`~@84(e2)@Q+~f52xNtR!=O*3({zz$pe$q6?7%|X8FJQ<6RbwNYgyO#1$#!*RF@=d1%c<7B8~7oaNe?CR-zANX}2Sa z)Wt}DAmv7-iXNb|f%sgxqUwQOyw2=q)zG=xE6Pz2+Hn{G- zA-#U;!&&eBIP^N<6M56%#tAQWsg6EX#8k0))KICxHapA!s2P4@%rpXZ5Cz3(GURhs z&rM!Osb@D|@cmHzsr`e6_&}ZRs|p`_oZDE-!Iu7u({ZwBb9nbwqwx~6^RPIv%Kfcl z$4v9ViW^*&E7)1X>BZ$)?C~c2O|+g*hL;H>&p6?|Tu@HZ?@RWAAWA^lTw~OHAci8>UC&CrN|a%oo$h4~eY%YLAQcW0}(`gH!2<51{Yrpv? zINag518a9kY{MRGJ5EVBk$Y>Nb=;W~MeiSUU;)#gc;ghCFz6!StLF}P3BL@35~~I8 zbY!>qsP-@3PPZ?`;AFwg+w{x|XqRhR$vEy%`eRfm$vAjk{eK0S89uC_v(`qrAx)~j zNQvA4P!~V_#k0~}?*vWk14bf+OhK>?cebY1aMr1jv>lal=}Bn*3}=|0G(%fYB=ISl zu(hOPMdKZtA>&4Rnah*}zZYT$Obv@Eg&f>mxdS@1w0=(woik_%xFY;}o)HDGegtVi m+v6Nj0Ul?Zl@tId{+3p Date: Mon, 23 Jan 2017 14:13:22 +0000 Subject: [PATCH 006/228] Update README --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31e00bf1..ccc66528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ #Change Log Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.com) and [Semantic Versioning](http://semver.org/spec/v2.0.0.html) + +## Next + +### Fixed + +- Simple implementation not reading DeletedObjects correctly per Nigel Rook +- Simple Implementation CustomIcons opional per @Kin-k + +### Added + +- Kdb Key File Support + ##[2.1.0] 2016-10-29 ### Added From 7daa1f94c3a5a0a17089b6e4d61efbf764d3243f Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 23 Jan 2017 14:36:25 +0000 Subject: [PATCH 007/228] Try to fix image problem in README --- CHANGELOG.md | 2 +- readme.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccc66528..d62cee08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.co ### Fixed - Simple implementation not reading DeletedObjects correctly per Nigel Rook -- Simple Implementation CustomIcons opional per @Kin-k +- Simple implementation CustomIcons optional per @Kin-k ### Added diff --git a/readme.md b/readme.md index 3b987702..7e8ba41f 100644 --- a/readme.md +++ b/readme.md @@ -148,7 +148,7 @@ Included POM is for Maven 3. There are rather a lot of modules, this is in order to allow loading of minimal necessary functionality. The module dependencies are illustrated below. -![Module Structure](Module Structure.svg) +![Module Structure](Module Structure.svg?raw=true) Each module corresponds to a Maven artifact. The GroupId is `org.linguafranca.pwdb`. The version id is as noted [above](#mvn). From 683cb5e481adacc53964befac6e8ad2f501f2671 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 23 Jan 2017 14:39:42 +0000 Subject: [PATCH 008/228] Another try at fixing module structure image --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 7e8ba41f..e1d1d442 100644 --- a/readme.md +++ b/readme.md @@ -148,7 +148,7 @@ Included POM is for Maven 3. There are rather a lot of modules, this is in order to allow loading of minimal necessary functionality. The module dependencies are illustrated below. -![Module Structure](Module Structure.svg?raw=true) +![Module Structure](Module Structure.svg?raw=true Module Structure) Each module corresponds to a Maven artifact. The GroupId is `org.linguafranca.pwdb`. The version id is as noted [above](#mvn). From d0bdba04f9edcd5d3e3759cd3e0228441fbba63d Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 23 Jan 2017 14:42:13 +0000 Subject: [PATCH 009/228] Once More ref Image --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index e1d1d442..d31d9c92 100644 --- a/readme.md +++ b/readme.md @@ -148,7 +148,7 @@ Included POM is for Maven 3. There are rather a lot of modules, this is in order to allow loading of minimal necessary functionality. The module dependencies are illustrated below. -![Module Structure](Module Structure.svg?raw=true Module Structure) +![Module Structure](Module Structure.svg "Module Structure") Each module corresponds to a Maven artifact. The GroupId is `org.linguafranca.pwdb`. The version id is as noted [above](#mvn). From 818e4082225315e48eb828a1bfc97227d31ad85a Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 23 Jan 2017 14:50:16 +0000 Subject: [PATCH 010/228] again ref image --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index d31d9c92..621a0a2c 100644 --- a/readme.md +++ b/readme.md @@ -148,7 +148,7 @@ Included POM is for Maven 3. There are rather a lot of modules, this is in order to allow loading of minimal necessary functionality. The module dependencies are illustrated below. -![Module Structure](Module Structure.svg "Module Structure") +![Module Structure](./Module Structure.svg "Module Structure") Each module corresponds to a Maven artifact. The GroupId is `org.linguafranca.pwdb`. The version id is as noted [above](#mvn). From a2d97853723c7d9fe785452408b3bcf4b720aed6 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 23 Jan 2017 18:17:57 +0000 Subject: [PATCH 011/228] Adding .travis.yml --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..b0513495 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: java +jdk: oraclejdk7 +script: mvn clean test From aeebbd3a1faaa22097cd9824f42d6186a90fba4b Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 23 Jan 2017 18:33:44 +0000 Subject: [PATCH 012/228] Add Travis build status --- readme.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 621a0a2c..65448f48 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,7 @@ # KeePassJava2 +Master: [![Build Status](https://travis-ci.org/jorabin/KeePassJava2.svg?branch=master)](https://travis-ci.org/jorabin/KeePassJava2) Develop: [![Build Status](https://travis-ci.org/jorabin/KeePassJava2.svg?branch=develop)](https://travis-ci.org/jorabin/KeePassJava2) + A Java 7 API for databases compatible with the renowned [KeePass](http://keepass.info) password safe for Windows. @@ -148,7 +150,7 @@ Included POM is for Maven 3. There are rather a lot of modules, this is in order to allow loading of minimal necessary functionality. The module dependencies are illustrated below. -![Module Structure](./Module Structure.svg "Module Structure") +[![Module Structure](./Module Structure.svg "Module Structure")](./Module Structure.svg) Each module corresponds to a Maven artifact. The GroupId is `org.linguafranca.pwdb`. The version id is as noted [above](#mvn). From 1b11a7b037a34f9e7ca94bc6b82e6904b32068b3 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 24 Jan 2017 08:42:21 +0000 Subject: [PATCH 013/228] further changes interworking with chromeipass --- .../linguafranca/pwdb/keepasshttp/Crypto.java | 12 +++++++----- .../pwdb/keepasshttp/KeePassJavaHttpHandler.java | 12 +++++++++--- .../linguafranca/pwdb/keepasshttp/Message.java | 10 ++++++---- .../linguafranca/pwdb/keepasshttp/Processor.java | 3 ++- http/src/test/java/TestRequestResponse.java | 16 ++++++++++++++++ 5 files changed, 40 insertions(+), 13 deletions(-) diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java index c047795e..78afbab8 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java @@ -78,11 +78,13 @@ public void makeVerifiable(Message.Response response) { response.Verifier = CryptoTransform(response.Nonce, false, true, cipher); */ - for (Message.ResponseEntry entry: response.Entries) { - entry.Login = encryptToBase64(entry.Login, iv); - entry.Uuid = encryptToBase64(entry.Uuid, iv); - entry.Name = encryptToBase64(entry.Name, iv); - entry.Password = encryptToBase64(entry.Password, iv); + if (response.Entries != null) { + for (Message.ResponseEntry entry : response.Entries) { + entry.Login = encryptToBase64(entry.Login, iv); + entry.Uuid = encryptToBase64(entry.Uuid, iv); + entry.Name = encryptToBase64(entry.Name, iv); + entry.Password = encryptToBase64(entry.Password, iv); + } } } diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java index 6ad5262e..5d0c8f2e 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java @@ -24,7 +24,7 @@ public class KeePassJavaHttpHandler extends AbstractHandler { private Logger logger = LoggerFactory.getLogger(KeePassJavaHttpHandler.class); - private Gson gson = new GsonBuilder().disableHtmlEscaping().create(); + private Gson gson = new GsonBuilder().disableHtmlEscaping().serializeNulls().create(); Processor processor = new Processor(); @Override @@ -59,11 +59,17 @@ public void handle(String s, Request request, HttpServletRequest httpServletRequ return; } - if (!processor.getCrypto().verify(request1)) { - httpServletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); + if (request1.RequestType.equals(Message.Type.ASSOCIATE)) { + processor.getCrypto().setKey(Helpers.decodeBase64Content(request1.Key.getBytes(), false)); + } + + if (request1.RequestType.equals(Message.Type.TEST_ASSOCIATE) && request1.Id == null || + !processor.getCrypto().verify(request1)) { + httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); response.Success = false; response.Error = "Request did not verify"; processor.getCrypto().makeVerifiable(response); + httpServletResponse.getWriter().write(gson.toJson(response)); request.setHandled(true); return; } diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java index 9d4c834e..7db766a4 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java @@ -27,12 +27,12 @@ public static class Verifiable { * Nonce value used in conjunction with all encrypted fields, * randomly generated for each request */ - public String Nonce; + public String Nonce=""; /** * Used to check that the correct key has been chosen */ - public String Verifier; + public String Verifier=""; } public static class Request extends Verifiable { @@ -103,7 +103,7 @@ public Response(String request, String hash) { */ public String RequestType; - public String Error = null; + public String Error = ""; public boolean Success = false; @@ -116,7 +116,7 @@ public Response(String request, String hash) { /** * response to get-logins-count, number of entries for requested Url */ - public int Count = 0; + public Integer Count; /** * response the current version of KeePassHttp @@ -132,6 +132,8 @@ public Response(String request, String hash) { * The resulting entries for a get-login request */ public List Entries; + +// public String objectName = ""; } public static class ResponseEntry { diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java index 15b98084..69a15c43 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java @@ -72,6 +72,7 @@ public void process(Message.Request request, Message.Response response) { response.Success = false; if (request.Id != null) { response.Success = request.Id.equals(adaptor.getId()); + response.Id = adaptor.getId(); } } } @@ -85,7 +86,7 @@ public void process(Message.Request r, Message.Response resp) { Helpers.base64FromUuid(UUID.randomUUID()))); resp.Id = adaptor.getId(); resp.Success = true; - resp.Count = 0; + resp.Count = resp.Entries.size(); /* string submithost = null; diff --git a/http/src/test/java/TestRequestResponse.java b/http/src/test/java/TestRequestResponse.java index 8266f94d..d52e7357 100644 --- a/http/src/test/java/TestRequestResponse.java +++ b/http/src/test/java/TestRequestResponse.java @@ -20,8 +20,24 @@ public class TestRequestResponse { private static String requestString = "{\"RequestType\":\"associate\",\"Key\":\"WQISYVajkviMNaSBIaUTgZbUaJD1RujU6eoh3B89+lc=\",\"Nonce\":\"V2bJ4VFRrbH+oUd0W8swjQ==\",\"Verifier\":\"oegZmDzOLFVv8jAYxfnOqBk+0KiasSr6J/8v9xbUIkE=\"}"; private static String getLoginsAssociate ="{\"RequestType\":\"associate\",\"Key\":\"W4z49b+uO2vLGzdqBzfKSY/ynmfLRcxfBYmk99OIT3k=\",\"Nonce\":\"9N3pLnqsXgyh3H7wGXrMQQ==\",\"Verifier\":\"b1z9Rhd8pYSUEczdPx4wWyQTtB9oQFjNzx5EfOfqdNw=\"}"; private static String getLogins ="{\"RequestType\":\"get-logins\",\"SortSelection\":\"true\",\"TriggerUnlock\":\"false\",\"Id\":\"1234\",\"Nonce\":\"v/93UGpWJYaglsrWTFsq6A==\",\"Verifier\":\"kltoi/9kt2rrv5SReMDYN7zX1XDeBdCPvU78m0qWqVQ=\",\"Url\":\"RNTCyUHMs3smRf99uQx0j/Y0T2dCDAEmYJyx1Gt9zhs=\",\"SubmitUrl\":\"RNTCyUHMs3smRf99uQx0jxIm9b/18UmkIR70L37lxckUpUNjh8Oxre5P6yc74d93OHG3f3OrYEpsItig+IXOAQ==\"}"; + + + private static String associate="{\"RequestType\":\"associate\",\"Key\":\"S7rQME6+2flB7ZJ01sH3aJYyq0KIDCpHtvu1vZspgXs=\",\"Nonce\":\"Z3fBN6zrgceHvnAb/OnwKg==\",\"Verifier\":\"ZQ+KrgSjICAMzocHizFhrJ/8/EyCnBcdypICtIrW1IU=\"}"; + private static String response = "{\"RequestType\":\"associate\",\"Error\":\"\",\"Success\":true,\"Id\":\"402881E9-58B6-5A30-0158-B65A30B20000\",\"Count\":null,\"Version\":\"1.8.4.1\",\"Hash\":\"d51377aeb06c1707f56c0b323662ddf41c777b0c\",\"Entries\":null,\"Nonce\":\"+XFkT0BRzkj/zW7N6W0g+w==\",\"Verifier\":\"0geZx858HFVFYl8b1gV4bAySuBasGnIbh4FdOv8Y/SM=\"}"; + private static String reTest = "{\"RequestType\":\"test-associate\",\"TriggerUnlock\":false,\"Id\":\"402881E9-58B6-5A30-0158-B65A30B20000\",\"Nonce\":\"+/UY+Mg4I8mSJweoNg1x+Q==\",\"Verifier\":\"GnhpnRBsCkT7LGcjZdBtMpgRWo3XFqf8Kwyxj4P3jbI=\"}"; + private static String reResponse = "{\"RequestType\":\"test-associate\",\"Error\":\"\",\"Success\":true,\"Id\":null,\"Count\":null,\"Version\":\"1.8.4.1\",\"Hash\":\"d51377aeb06c1707f56c0b323662ddf41c777b0c\",\"Entries\":null,\"Nonce\":\"QaYPVoWvF+k31MQGuyLEEA==\",\"Verifier\":\"kOReuG3a0l+do6/8xC58QjNVqgMrAADvt7agyczuPtk=\"}"; private Gson gson = new GsonBuilder().disableHtmlEscaping().create(); + @Test + public void testVerifyAssociate() { + Message.Request r = gson.fromJson(associate, Message.Request.class); + Processor processor = new Processor(r.Key); + assertTrue(processor.getCrypto().verify(r)); + assertTrue(processor.getCrypto().verify(gson.fromJson(response, Message.Response.class))); + assertTrue(processor.getCrypto().verify(gson.fromJson(reTest, Message.Request.class))); + assertTrue(processor.getCrypto().verify(gson.fromJson(reResponse, Message.Response.class))); + } + @Test public void testVerifyAssociateRequestResponse() { From fd27643b6f94cb77aa9d7e4b65a4f24ca726f330 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 24 Jan 2017 10:07:09 +0000 Subject: [PATCH 014/228] Correct test --- http/src/test/java/MonitoredRequestsTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/http/src/test/java/MonitoredRequestsTest.java b/http/src/test/java/MonitoredRequestsTest.java index 2a370834..e8c51ad8 100644 --- a/http/src/test/java/MonitoredRequestsTest.java +++ b/http/src/test/java/MonitoredRequestsTest.java @@ -34,6 +34,8 @@ public void sessionTest() { processor.getHandler(request.RequestType).process(request, computedResponse); computedResponse.Id = "NEW coNECTION"; + processor.getCrypto().makeVerifiable(computedResponse); + System.out.println(gson.toJson(computedResponse)); processor.getCrypto().verify(computedResponse); System.out.println(gson.toJson(actualResponse)); @@ -46,6 +48,7 @@ public void sessionTest() { computedResponse = new Message.Response(request.RequestType, "eefc4faf792a59d034da69c69f3643ef34ba7d8d"); processor.getHandler(request.RequestType).process(request, computedResponse); + processor.getCrypto().makeVerifiable(computedResponse); computedResponse.Id = "NEW coNECTION"; System.out.println(gson.toJson(computedResponse)); @@ -59,6 +62,7 @@ public void sessionTest() { computedResponse = new Message.Response(request.RequestType, "eefc4faf792a59d034da69c69f3643ef34ba7d8d"); processor.getHandler(request.RequestType).process(request, computedResponse); + processor.getCrypto().makeVerifiable(computedResponse); computedResponse.Id = "NEW coNECTION"; System.out.println(gson.toJson(computedResponse)); From 5e3de4cc9f58751f67a1c76a73acc850d14dfb78 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 24 Jan 2017 13:38:52 +0000 Subject: [PATCH 015/228] now seems to work a bit with ChromIPass --- .../linguafranca/pwdb/keepasshttp/Crypto.java | 6 +++++ .../keepasshttp/KeePassJavaHttpHandler.java | 25 ++++++++----------- .../pwdb/keepasshttp/Message.java | 2 +- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java index 78afbab8..a3aeff74 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java @@ -45,7 +45,13 @@ public boolean verify(Message.Verifiable verifiable) { return false; } byte[] verifier = Helpers.decodeBase64Content(verifiable.Verifier.getBytes(), false); + if (verifier.length == 0) { + return false; + } byte[] iv = Helpers.decodeBase64Content(verifiable.Nonce.getBytes(), false); + if (iv.length == 0) { + return false; + } PaddedBufferedBlockCipher cipher = getCipher(Crypto.CMode.DECRYPT, iv); diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java index 5d0c8f2e..8d8c4af2 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java @@ -63,26 +63,21 @@ public void handle(String s, Request request, HttpServletRequest httpServletRequ processor.getCrypto().setKey(Helpers.decodeBase64Content(request1.Key.getBytes(), false)); } - if (request1.RequestType.equals(Message.Type.TEST_ASSOCIATE) && request1.Id == null || - !processor.getCrypto().verify(request1)) { - httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); + OutputStream outputStream = new LogginOutputStream(httpServletResponse.getOutputStream(), logger); + Writer writer = new BufferedWriter(new OutputStreamWriter(outputStream)); + + if (!processor.getCrypto().verify(request1)) { + logger.info("Request failed verification"); response.Success = false; - response.Error = "Request did not verify"; + response.Error = ""; + response.Hash=""; + } else { + handler.process(request1, response); processor.getCrypto().makeVerifiable(response); - httpServletResponse.getWriter().write(gson.toJson(response)); - request.setHandled(true); - return; } - - handler.process(request1, response); - - processor.getCrypto().makeVerifiable(response); - - OutputStream outputStream = new LogginOutputStream(httpServletResponse.getOutputStream(), logger); - Writer writer = new BufferedWriter(new OutputStreamWriter(outputStream)); + httpServletResponse.setStatus(HttpServletResponse.SC_OK); gson.toJson(response, writer); writer.flush(); - httpServletResponse.setStatus(HttpServletResponse.SC_OK); request.setHandled(true); } diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java index 7db766a4..5bd0e3c6 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java @@ -111,7 +111,7 @@ public Response(String request, String hash) { * The user selected String as a result of 'associate', * always returned on every request */ - public String Id; + public String Id=""; /** * response to get-logins-count, number of entries for requested Url From d5edd64571642ace3b54ed14632ab26d5b9ec471 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 24 Jan 2017 17:04:36 +0000 Subject: [PATCH 016/228] Comments and tidy --- .../linguafranca/pwdb/keepasshttp/Crypto.java | 2 +- ...tpHandler.java => KeePassHttpHandler.java} | 21 +++++++++---------- ...ettyServer.java => KeePassHttpServer.java} | 6 +++--- .../pwdb/keepasshttp/util/LogginProxy.java | 4 +--- 4 files changed, 15 insertions(+), 18 deletions(-) rename http/src/main/java/org/linguafranca/pwdb/keepasshttp/{KeePassJavaHttpHandler.java => KeePassHttpHandler.java} (85%) rename http/src/main/java/org/linguafranca/pwdb/keepasshttp/{KeePassJavaJettyServer.java => KeePassHttpServer.java} (81%) diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java index a3aeff74..1e861379 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java @@ -12,7 +12,7 @@ import java.util.Arrays; /** - * @author jo + * Cryptography for KeePassHttp emulator */ public class Crypto { diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java similarity index 85% rename from http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java rename to http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java index 8d8c4af2..aaee97b1 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaHttpHandler.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java @@ -2,7 +2,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import org.apache.commons.codec.binary.Hex; +import com.google.gson.stream.JsonWriter; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.AbstractHandler; import org.linguafranca.pwdb.kdbx.Helpers; @@ -10,22 +10,20 @@ import org.linguafranca.pwdb.keepasshttp.util.LogginOutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.spongycastle.crypto.digests.SHA1Digest; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.*; -import java.util.UUID; /** - * @author jo + * Jetty Handler for PassIFox and ChromeIPass clients - emulates KeePassHttp plugin. */ -public class KeePassJavaHttpHandler extends AbstractHandler { +public class KeePassHttpHandler extends AbstractHandler { - private Logger logger = LoggerFactory.getLogger(KeePassJavaHttpHandler.class); + private Logger logger = LoggerFactory.getLogger(KeePassHttpHandler.class); private Gson gson = new GsonBuilder().disableHtmlEscaping().serializeNulls().create(); - Processor processor = new Processor(); + private Processor processor = new Processor(); @Override public void handle(String s, Request request, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException, ServletException { @@ -63,11 +61,9 @@ public void handle(String s, Request request, HttpServletRequest httpServletRequ processor.getCrypto().setKey(Helpers.decodeBase64Content(request1.Key.getBytes(), false)); } - OutputStream outputStream = new LogginOutputStream(httpServletResponse.getOutputStream(), logger); - Writer writer = new BufferedWriter(new OutputStreamWriter(outputStream)); - + // normal part of the protocol to fail verfication on test-associate if (!processor.getCrypto().verify(request1)) { - logger.info("Request failed verification"); + logger.debug("Request failed verification"); response.Success = false; response.Error = ""; response.Hash=""; @@ -75,6 +71,9 @@ public void handle(String s, Request request, HttpServletRequest httpServletRequ handler.process(request1, response); processor.getCrypto().makeVerifiable(response); } + // create a logging outputstream to see what is being sent + OutputStream outputStream = new LogginOutputStream(httpServletResponse.getOutputStream(), logger); + Writer writer = new BufferedWriter(new OutputStreamWriter(outputStream)); httpServletResponse.setStatus(HttpServletResponse.SC_OK); gson.toJson(response, writer); writer.flush(); diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaJettyServer.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpServer.java similarity index 81% rename from http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaJettyServer.java rename to http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpServer.java index 753130e1..7f6c1e9a 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassJavaJettyServer.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpServer.java @@ -4,9 +4,9 @@ import org.eclipse.jetty.server.ServerConnector; /** - * @author jo + * Embedded server */ -public class KeePassJavaJettyServer { +public class KeePassHttpServer { public static void main(String[] args) throws Exception { Server server = new Server(); ServerConnector http = new ServerConnector(server); @@ -14,7 +14,7 @@ public static void main(String[] args) throws Exception { http.setPort(19455); http.setIdleTimeout(300000); server.addConnector(http); - server.setHandler(new KeePassJavaHttpHandler()); + server.setHandler(new KeePassHttpHandler()); server.start(); server.join(); } diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginProxy.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginProxy.java index 32b48b4f..c04a4469 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginProxy.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginProxy.java @@ -100,9 +100,7 @@ public Boolean call() throws Exception { forwardSocket.shutdownOutput(); clientSocket.shutdownOutput(); logger.info("Connection finished"); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { + } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } return true; From 0ef2eaca9c27103efcdf6ca809bb5814e7222a19 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 24 Jan 2017 18:03:30 +0000 Subject: [PATCH 017/228] Tidy up Crypto --- .../linguafranca/pwdb/keepasshttp/Crypto.java | 95 +++++++++++-------- .../keepasshttp}/MonitoredRequestsTest.java | 2 + .../keepasshttp}/TestRequestResponse.java | 13 ++- 3 files changed, 67 insertions(+), 43 deletions(-) rename http/src/test/java/{ => org/linguafranca/pwdb/keepasshttp}/MonitoredRequestsTest.java (99%) rename http/src/test/java/{ => org/linguafranca/pwdb/keepasshttp}/TestRequestResponse.java (91%) diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java index 1e861379..9ddfd522 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java @@ -9,22 +9,23 @@ import org.spongycastle.crypto.params.ParametersWithIV; import java.security.SecureRandom; -import java.util.Arrays; /** * Cryptography for KeePassHttp emulator */ -public class Crypto { +class Crypto { private byte[] binaryKey; - public byte[] getKey() { + + byte[] getKey() { return binaryKey; } - public void setKey(byte[] binaryKey) { + + void setKey(byte[] binaryKey) { this.binaryKey = binaryKey; } - public enum CMode { + enum CMode { ENCRYPT(true), DECRYPT(false); @@ -39,51 +40,40 @@ public boolean getEncrypt() { } } - /** TODO this can be simplified using the helpers below (it's what they are for) */ - public boolean verify(Message.Verifiable verifiable) { - if (getKey() == null || verifiable.Verifier == null || verifiable.Nonce == null) { - return false; - } - byte[] verifier = Helpers.decodeBase64Content(verifiable.Verifier.getBytes(), false); - if (verifier.length == 0) { + /** + * Return true if the Nonce and the Verifier on a message match + * @param verifiable a message containing those fields + */ + boolean verify(Message.Verifiable verifiable) { + if (getKey() == null || verifiable.Verifier == null || verifiable.Nonce == null || + verifiable.Verifier.equals("") || verifiable.Nonce.equals("")) { return false; } + // The nonce is base64 encoded version of an iv byte[] iv = Helpers.decodeBase64Content(verifiable.Nonce.getBytes(), false); - if (iv.length == 0) { - return false; - } - - PaddedBufferedBlockCipher cipher = getCipher(Crypto.CMode.DECRYPT, iv); - - byte[] output = new byte[cipher.getOutputSize(verifier.length)]; - int outputlen = cipher.processBytes(verifier, 0, verifier.length, output, 0); - - try { - cipher.doFinal(output, outputlen); - byte[] comparison = new byte[output.length]; - System.arraycopy(verifiable.Nonce.getBytes(),0,comparison,0,verifiable.Nonce.length()); - return Arrays.equals(output, comparison); - } catch (InvalidCipherTextException e) { - return false; - } + // The verifier is the base64 encoded encrypted version of the nonce + String decrypted = decryptFromBase64(verifiable.Verifier, iv); + // the decrypted verifier should be the same as the nonce + return decrypted.equals(verifiable.Nonce); } - public void makeVerifiable(Message.Response response) { + /** + * Add a Nonce and a Verifier to a message to make it verifiable + * @param response a message to make verifiable + */ + void makeVerifiable(Message.Response response) { // we don't have a key? we can't do anything if (getKey() == null) { return; } // The nonce is base64 encoded version of an iv - // The verifier is the base64 encoded encrypted version of the nonce byte[] iv = new SecureRandom().generateSeed(16); response.Nonce = Helpers.encodeBase64Content(iv, false); + // The verifier is the base64 encoded encrypted version of the nonce response.Verifier = encryptToBase64(response.Nonce, iv); -/* - PaddedBufferedBlockCipher cipher = getCipher(Crypto.CMode.ENCRYPT, iv); - response.Verifier = CryptoTransform(response.Nonce, false, true, cipher); -*/ + // encrypt any entries if (response.Entries != null) { for (Message.ResponseEntry entry : response.Entries) { entry.Login = encryptToBase64(entry.Login, iv); @@ -94,22 +84,47 @@ public void makeVerifiable(Message.Response response) { } } - - public PaddedBufferedBlockCipher getCipher(CMode mode, byte[] iv) { + /** + * Get a cipher + * @param mode encryption or decryption + * @param iv a 16 byte iv + * @return an initialised Cipher + */ + PaddedBufferedBlockCipher getCipher(CMode mode, byte[] iv) { PaddedBufferedBlockCipher result = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); result.init(mode.getEncrypt(), new ParametersWithIV(new KeyParameter(getKey()), iv)); return result; } - public String decryptFromBase64(String input, byte[] iv){ + /** + * Return an unencrypted non encoded copy of an encrypted base 64 encoded string + * @param input cipher text + * @param iv an iv + * @return plain text + */ + String decryptFromBase64(String input, byte[] iv){ return CryptoTransform(input, true, false, getCipher(CMode.DECRYPT, iv)); } - public String encryptToBase64(String input, byte[] iv){ + /** + * Return an encrypted base 64 encoded copy of plain text string + * @param input plain text + * @param iv an iv + * @return cipher text + */ + String encryptToBase64(String input, byte[] iv){ return CryptoTransform(input, false, true, getCipher(CMode.ENCRYPT, iv)); } - public static String CryptoTransform(String input, boolean base64in, boolean base64out, PaddedBufferedBlockCipher cipher) { + /** + * Encryption and Decryption Helper + * @param input the candidate for transformation + * @param base64in true if base 64 encoded + * @param base64out true if we require base 64 out + * @param cipher a Cipher initialised for Encrypt or Decrypt + * @return the transformed result + */ + static String CryptoTransform(String input, boolean base64in, boolean base64out, PaddedBufferedBlockCipher cipher) { byte[] bytes; if (base64in) { bytes = Helpers.decodeBase64Content(input.getBytes(), false); diff --git a/http/src/test/java/MonitoredRequestsTest.java b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java similarity index 99% rename from http/src/test/java/MonitoredRequestsTest.java rename to http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java index e8c51ad8..9cb79151 100644 --- a/http/src/test/java/MonitoredRequestsTest.java +++ b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java @@ -1,3 +1,5 @@ +package org.linguafranca.pwdb.keepasshttp; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; import org.junit.Test; diff --git a/http/src/test/java/TestRequestResponse.java b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java similarity index 91% rename from http/src/test/java/TestRequestResponse.java rename to http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java index d52e7357..8f447935 100644 --- a/http/src/test/java/TestRequestResponse.java +++ b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java @@ -1,3 +1,5 @@ +package org.linguafranca.pwdb.keepasshttp; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; import org.junit.Test; @@ -7,11 +9,10 @@ import org.linguafranca.pwdb.keepasshttp.Message; import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; +import java.security.SecureRandom; import java.util.ArrayList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; /** * @author jo @@ -32,6 +33,12 @@ public class TestRequestResponse { public void testVerifyAssociate() { Message.Request r = gson.fromJson(associate, Message.Request.class); Processor processor = new Processor(r.Key); + assertArrayEquals(r.Key.getBytes(), Helpers.encodeBase64Content(processor.getCrypto().getKey()).getBytes()); + + byte[] iv = new SecureRandom().generateSeed(16); + String secret = processor.getCrypto().encryptToBase64("Secret", iv); + assertEquals("Secret", processor.getCrypto().decryptFromBase64(secret, iv)); + assertTrue(processor.getCrypto().verify(r)); assertTrue(processor.getCrypto().verify(gson.fromJson(response, Message.Response.class))); assertTrue(processor.getCrypto().verify(gson.fromJson(reTest, Message.Request.class))); From 657dd6e647068acdfc3648e61a1d130b426d21ea Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 24 Jan 2017 18:03:55 +0000 Subject: [PATCH 018/228] Fix stack overflow exception in Helpers --- kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java index 468662d6..9afefd58 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java @@ -111,7 +111,7 @@ public static byte[] unzipBinaryContent(byte[] content) { } public static String encodeBase64Content(byte[] value) { - return encodeBase64Content(value); + return encodeBase64Content(value, false); } public static String encodeBase64Content(byte[] value, boolean isCompressed) { From 9a7f4fbe12959e850ca977449a564ce25f7a8c59 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 24 Jan 2017 18:48:37 +0000 Subject: [PATCH 019/228] Further tidying up --- .../linguafranca/pwdb/keepasshttp/Crypto.java | 6 + .../pwdb/keepasshttp/Handlers.java | 792 ------------------ .../pwdb/keepasshttp/KeePassHttpHandler.java | 33 +- .../pwdb/keepasshttp/Message.java | 21 +- .../pwdb/keepasshttp/Processor.java | 223 +---- .../keepasshttp/MonitoredRequestsTest.java | 33 +- .../pwdb/keepasshttp/TestRequestResponse.java | 43 +- 7 files changed, 114 insertions(+), 1037 deletions(-) delete mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/Handlers.java diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java index 9ddfd522..f3ab44d3 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java @@ -17,6 +17,12 @@ class Crypto { private byte[] binaryKey; + Crypto () {} + + Crypto (String key) { + this.binaryKey = Helpers.decodeBase64Content(key.getBytes()); + } + byte[] getKey() { return binaryKey; } diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Handlers.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Handlers.java deleted file mode 100644 index 343a7d48..00000000 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Handlers.java +++ /dev/null @@ -1,792 +0,0 @@ -package org.linguafranca.pwdb.keepasshttp; - -/** - * @author jo - */ -public class Handlers { -/* - public sealed partial class KeePassHttpExt : Plugin - { - private string GetHost(string uri) - { - var host = uri; - try - { - var url = new Uri(uri); - host = url.Host; - - if (!url.IsDefaultPort) - { - host += ":" + url.Port.ToString(); - } - } - catch - { - // ignore exception, not a URI, assume input is host - } - return host; - } - - private string GetScheme(string uri) - { - var scheme = ""; - try - { - var url = new Uri(uri); - scheme = url.Scheme; - } - catch - { - // ignore exception, not a URI, assume input is host - } - return scheme; - } - - private bool canShowBalloonTips() - { - // tray icon is not visible --> no balloon tips for it - if (Program.Config.UI.TrayIcon.ShowOnlyIfTrayed && !host.MainWindow.IsTrayed()) - { - return false; - } - - // only use balloon tips on windows machines - if (Environment.OSVersion.Platform == PlatformID.Win32NT || Environment.OSVersion.Platform == System.PlatformID.Win32S || Environment.OSVersion.Platform == System.PlatformID.Win32Windows) - { - int enabledBalloonTipsMachine = (int)Registry.GetValue("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced", - "EnableBalloonTips", - 1); - int enabledBalloonTipsUser = (int)Registry.GetValue("HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced", - "EnableBalloonTips", - 1); - return (enabledBalloonTipsMachine == 1 && enabledBalloonTipsUser == 1); - } - - return false; - } - - 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 - - root.SearchEntries(parms, list); - foreach (var entry in list) - { - var name = entry.Strings.ReadSafe(PwDefs.TitleField); - var login = GetUserPass(entry)[0]; - var uuid = entry.Uuid.ToHexString(); - var e = new ResponseEntry(name, login, null, uuid, null); - resp.Entries.Add(e); - } - 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); - } - } - - private IEnumerable FindMatchingEntries(Request r, Aes aes) - { - string submitHost = null; - string realm = null; - var listResult = new List(); - var url = CryptoTransform(r.Url, true, false, aes, CMode.DECRYPT); - string formHost, searchHost; - formHost = searchHost = GetHost(url); - string hostScheme = GetScheme(url); - if (r.SubmitUrl != null) { - submitHost = GetHost(CryptoTransform(r.SubmitUrl, true, false, aes, CMode.DECRYPT)); - } - if (r.Realm != null) - realm = CryptoTransform(r.Realm, true, false, aes, CMode.DECRYPT); - - var origSearchHost = searchHost; - var parms = MakeSearchParameters(); - - 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); - } - - 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 = 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; - } - listCount = listResult.Count; - } - - - 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) - { - if (c.Allow.Contains(formHost) && (submitHost == null || c.Allow.Contains(submitHost))) - return true; - if (c.Deny.Contains(formHost) || (submitHost != null && c.Deny.Contains(submitHost))) - return false; - if (realm != null && c.Realm != realm) - return false; - } - - if (entryUrl != null && (entryUrl.StartsWith("http://") || entryUrl.StartsWith("https://") || title.StartsWith("ftp://") || title.StartsWith("sftp://"))) - { - var uHost = GetHost(entryUrl); - if (formHost.EndsWith(uHost)) - return true; - } - - if (title.StartsWith("http://") || title.StartsWith("https://") || title.StartsWith("ftp://") || title.StartsWith("sftp://")) - { - var uHost = GetHost(title); - if (formHost.EndsWith(uHost)) - return true; - } - return formHost.Contains(title) || (entryUrl != null && formHost.Contains(entryUrl)); - }; - - Func filterSchemes = delegate(PwEntry e) - { - var title = e.Strings.ReadSafe(PwDefs.TitleField); - var entryUrl = e.Strings.ReadSafe(PwDefs.UrlField); - - if (entryUrl != null) - { - var entryScheme = GetScheme(entryUrl); - if (entryScheme == hostScheme) - { - return true; - } - } - - var titleScheme = GetScheme(title); - if (titleScheme == hostScheme) - { - return true; - } - - return false; - }; - - var result = from e in listResult where filter(e.entry) select e; - - if (configOpt.MatchSchemes) - { - result = from e in result where filterSchemes(e.entry) select e; - } - - return result; - } - - private void GetLoginsCountHandler(Request r, Response resp, Aes aes) - { - if (!VerifyRequest(r, aes)) - return; - - resp.Success = true; - resp.Id = r.Id; - var items = FindMatchingEntries(r, aes); - SetResponseVerifier(resp, aes); - resp.Count = items.ToList().Count; - } - - private void GetLoginsHandler(Request r, Response resp, Aes aes) - { - if (!VerifyRequest(r, aes)) - return; - - string submithost = null; - var host = GetHost(CryptoTransform(r.Url, true, false, aes, CMode.DECRYPT)); - if (r.SubmitUrl != null) - submithost = GetHost(CryptoTransform(r.SubmitUrl, true, false, aes, CMode.DECRYPT)); - - var items = FindMatchingEntries(r, aes); - if (items.ToList().Count > 0) - { - Func filter = delegate(PwEntry e) - { - var c = GetEntryConfig(e); - - var title = e.Strings.ReadSafe(PwDefs.TitleField); - var entryUrl = e.Strings.ReadSafe(PwDefs.UrlField); - if (c != null) - { - return title != host && entryUrl != host && !c.Allow.Contains(host) || (submithost != null && !c.Allow.Contains(submithost) && submithost != title && submithost != entryUrl); - } - return title != host && entryUrl != host || (submithost != null && title != submithost && entryUrl != submithost); - }; - - var configOpt = new ConfigOpt(this.host.CustomConfig); - var config = GetConfigEntry(true); - var autoAllowS = config.Strings.ReadSafe("Auto Allow"); - var autoAllow = autoAllowS != null && autoAllowS.Trim() != ""; - autoAllow = autoAllow || configOpt.AlwaysAllowAccess; - var needPrompting = from e in items where filter(e.entry) select e; - - if (needPrompting.ToList().Count > 0 && !autoAllow) - { - var win = this.host.MainWindow; - - using (var f = new AccessControlForm()) - { - win.Invoke((MethodInvoker)delegate - { - f.Icon = win.Icon; - f.Plugin = this; - f.Entries = (from e in items where filter(e.entry) select e.entry).ToList(); - //f.Entries = needPrompting.ToList(); - f.Host = submithost != null ? submithost : host; - f.Load += delegate { f.Activate(); }; - f.ShowDialog(win); - if (f.Remember && (f.Allowed || f.Denied)) - { - foreach (var e in needPrompting) - { - var c = GetEntryConfig(e.entry); - if (c == null) - c = new KeePassHttpEntryConfig(); - var set = f.Allowed ? c.Allow : c.Deny; - set.Add(host); - if (submithost != null && submithost != host) - set.Add(submithost); - SetEntryConfig(e.entry, c); - - } - } - if (!f.Allowed) - { - items = items.Except(needPrompting); - } - }); - } - } - - string compareToUrl = null; - if (r.SubmitUrl != null) - { - compareToUrl = CryptoTransform(r.SubmitUrl, true, false, aes, CMode.DECRYPT); - } - if(String.IsNullOrEmpty(compareToUrl)) - compareToUrl = CryptoTransform(r.Url, true, false, aes, CMode.DECRYPT); - - compareToUrl = compareToUrl.ToLower(); - - foreach (var entryDatabase in items) - { - string entryUrl = String.Copy(entryDatabase.entry.Strings.ReadSafe(PwDefs.UrlField)); - if (String.IsNullOrEmpty(entryUrl)) - entryUrl = entryDatabase.entry.Strings.ReadSafe(PwDefs.TitleField); - - entryUrl = entryUrl.ToLower(); - - entryDatabase.entry.UsageCount = (ulong)LevenshteinDistance(compareToUrl, entryUrl); - - } - - var itemsList = items.ToList(); - - if (configOpt.SpecificMatchingOnly) - { - itemsList = (from e in itemsList - orderby e.entry.UsageCount ascending - select e).ToList(); - - ulong lowestDistance = itemsList.Count > 0 ? - itemsList[0].entry.UsageCount : - 0; - - itemsList = (from e in itemsList - where e.entry.UsageCount == lowestDistance - orderby e.entry.UsageCount - select e).ToList(); - - } - - if (configOpt.SortResultByUsername) - { - var items2 = from e in itemsList orderby e.entry.UsageCount ascending, GetUserPass(e)[0] ascending select e; - itemsList = items2.ToList(); - } - else - { - var items2 = from e in itemsList orderby e.entry.UsageCount ascending, e.entry.Strings.ReadSafe(PwDefs.TitleField) ascending select e; - 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; - } - else - { - resp.Success = true; - resp.Id = r.Id; - SetResponseVerifier(resp, aes); - } - } - //http://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#C.23 - private int LevenshteinDistance(string source, string target) - { - if (String.IsNullOrEmpty(source)) - { - if (String.IsNullOrEmpty(target)) return 0; - return target.Length; - } - if (String.IsNullOrEmpty(target)) return source.Length; - - if (source.Length > target.Length) - { - var temp = target; - target = source; - source = temp; - } - - var m = target.Length; - var n = source.Length; - var distance = new int[2, m + 1]; - // Initialize the distance 'matrix' - for (var j = 1; j <= m; j++) distance[0, j] = j; - - var currentRow = 0; - for (var i = 1; i <= n; ++i) - { - currentRow = i & 1; - distance[currentRow, 0] = i; - var previousRow = currentRow ^ 1; - for (var j = 1; j <= m; j++) - { - var cost = (target[j - 1] == source[i - 1] ? 0 : 1); - distance[currentRow, j] = Math.Min(Math.Min( - distance[previousRow, j] + 1, - distance[currentRow, j - 1] + 1), - distance[previousRow, j - 1] + cost); - } - } - return distance[currentRow, m]; - } - - private ResponseEntry PrepareElementForResponseEntries(ConfigOpt configOpt, PwEntryDatabase entryDatabase) - { - var name = entryDatabase.entry.Strings.ReadSafe(PwDefs.TitleField); - var loginpass = GetUserPass(entryDatabase); - var login = loginpass[0]; - var passwd = loginpass[1]; - var uuid = entryDatabase.entry.Uuid.ToHexString(); - - List fields = null; - if (configOpt.ReturnStringFields) - { - fields = new List(); - foreach (var sf in entryDatabase.entry.Strings) - { - var sfValue = entryDatabase.entry.Strings.ReadSafe(sf.Key); - if (configOpt.ReturnStringFieldsWithKphOnly) - { - if (sf.Key.StartsWith("KPH: ")) - { - fields.Add(new ResponseStringField(sf.Key.Substring(5), sfValue)); - } - } - else - { - fields.Add(new ResponseStringField(sf.Key, sfValue)); - } - } - - if (fields.Count > 0) - { - var fields2 = from e2 in fields orderby e2.Key ascending select e2; - fields = fields2.ToList(); - } - else - { - fields = null; - } - } - - return new ResponseEntry(name, login, passwd, uuid, fields); - } - - private void SetLoginHandler(Request r, Response resp, Aes aes) - { - if (!VerifyRequest(r, aes)) - return; - - string url = CryptoTransform(r.Url, true, false, aes, CMode.DECRYPT); - var urlHost = GetHost(url); - - PwUuid uuid = null; - string username, password; - - username = CryptoTransform(r.Login, true, false, aes, CMode.DECRYPT); - password = CryptoTransform(r.Password, true, false, aes, CMode.DECRYPT); - - if (r.Uuid != null) - { - uuid = new PwUuid(MemUtil.HexStringToByteArray( - CryptoTransform(r.Uuid, true, false, aes, CMode.DECRYPT))); - } - - if (uuid != null) - { - // modify existing entry - UpdateEntry(uuid, username, password, urlHost, r.Id); - } - else - { - // create new entry - CreateEntry(username, password, urlHost, url, r, aes); - } - - resp.Success = true; - resp.Id = r.Id; - SetResponseVerifier(resp, aes); - } - - private void AssociateHandler(Request r, Response resp, Aes aes) - { - if (!TestRequestVerifier(r, aes, r.Key)) - return; - - // key is good, prompt user to save - using (var f = new ConfirmAssociationForm()) - { - var win = host.MainWindow; - win.Invoke((MethodInvoker)delegate - { - f.Activate(); - f.Icon = win.Icon; - f.Key = r.Key; - f.Load += delegate { f.Activate(); }; - f.ShowDialog(win); - - if (f.KeyId != null) - { - var entry = GetConfigEntry(true); - - bool keyNameExists = true; - while (keyNameExists) - { - DialogResult keyExistsResult = DialogResult.Yes; - foreach (var s in entry.Strings) - { - if (s.Key == ASSOCIATE_KEY_PREFIX + f.KeyId) - { - keyExistsResult = MessageBox.Show( - win, - "A shared encryption-key with the name \"" + f.KeyId + "\" already exists.\nDo you want to overwrite it?", - "Overwrite existing key?", - MessageBoxButtons.YesNo, - MessageBoxIcon.Warning, - MessageBoxDefaultButton.Button1 - ); - break; - } - } - - if (keyExistsResult == DialogResult.No) - { - f.ShowDialog(win); - } - else - { - keyNameExists = false; - } - } - - if (f.KeyId != null) - { - entry.Strings.Set(ASSOCIATE_KEY_PREFIX + f.KeyId, new ProtectedString(true, r.Key)); - entry.Touch(true); - resp.Id = f.KeyId; - resp.Success = true; - SetResponseVerifier(resp, aes); - UpdateUI(null); - } - } - }); - } - } - - private void TestAssociateHandler(Request r, Response resp, Aes aes) - { - if (!VerifyRequest(r, aes)) - return; - - resp.Success = true; - resp.Id = r.Id; - SetResponseVerifier(resp, aes); - } - - private void GeneratePassword(Request r, Response resp, Aes aes) - { - if (!VerifyRequest(r, aes)) - return; - - byte[] pbEntropy = null; - ProtectedString psNew; - PwProfile autoProfile = Program.Config.PasswordGenerator.AutoGeneratedPasswordsProfile; - PwGenerator.Generate(out psNew, autoProfile, pbEntropy, Program.PwGeneratorPool); - - byte[] pbNew = psNew.ReadUtf8(); - if (pbNew != null) - { - uint uBits = QualityEstimation.EstimatePasswordBits(pbNew); - ResponseEntry item = new ResponseEntry(Request.GENERATE_PASSWORD, uBits.ToString(), StrUtil.Utf8.GetString(pbNew), Request.GENERATE_PASSWORD, null); - resp.Entries.Add(item); - resp.Success = true; - resp.Count = 1; - MemUtil.ZeroByteArray(pbNew); - } - - 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); - } - } - - private KeePassHttpEntryConfig GetEntryConfig(PwEntry e) - { - var serializer = NewJsonSerializer(); - if (e.Strings.Exists(KEEPASSHTTP_NAME)) - { - var json = e.Strings.ReadSafe(KEEPASSHTTP_NAME); - using (var ins = new JsonTextReader(new StringReader(json))) - { - return serializer.Deserialize(ins); - } - } - return null; - } - - private void SetEntryConfig(PwEntry e, KeePassHttpEntryConfig c) - { - var serializer = NewJsonSerializer(); - var writer = new StringWriter(); - serializer.Serialize(writer, c); - e.Strings.Set(KEEPASSHTTP_NAME, new ProtectedString(false, writer.ToString())); - e.Touch(true); - UpdateUI(e.ParentGroup); - } - - private bool UpdateEntry(PwUuid uuid, string username, string password, string formHost, string requestId) - { - PwEntry entry = null; - - var configOpt = new ConfigOpt(this.host.CustomConfig); - if (configOpt.SearchInAllOpenedDatabases) - { - foreach (PwDocument doc in host.MainWindow.DocumentManager.Documents) - { - if (doc.Database.IsOpen) - { - entry = doc.Database.RootGroup.FindEntry(uuid, true); - if (entry != null) - { - break; - } - } - } - } - else - { - entry = host.Database.RootGroup.FindEntry(uuid, true); - } - - if (entry == null) - { - return false; - } - - string[] up = GetUserPass(entry); - var u = up[0]; - var p = up[1]; - - if (u != username || p != password) - { - bool allowUpdate = configOpt.AlwaysAllowUpdates; - - if (!allowUpdate) - { - host.MainWindow.Activate(); - - DialogResult result; - if (host.MainWindow.IsTrayed()) - { - result = MessageBox.Show( - String.Format("Do you want to update the information in {0} - {1}?", formHost, u), - "Update Entry", MessageBoxButtons.YesNo, - MessageBoxIcon.None, MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly); - } - else - { - result = MessageBox.Show( - host.MainWindow, - String.Format("Do you want to update the information in {0} - {1}?", formHost, u), - "Update Entry", MessageBoxButtons.YesNo, - MessageBoxIcon.Information, MessageBoxDefaultButton.Button1); - } - - - if (result == DialogResult.Yes) - { - allowUpdate = true; - } - } - - if (allowUpdate) - { - PwObjectList m_vHistory = entry.History.CloneDeep(); - entry.History = m_vHistory; - entry.CreateBackup(null); - - entry.Strings.Set(PwDefs.UserNameField, new ProtectedString(false, username)); - entry.Strings.Set(PwDefs.PasswordField, new ProtectedString(true, password)); - entry.Touch(true, false); - UpdateUI(entry.ParentGroup); - - return true; - } - } - - return false; - } - - private bool CreateEntry(string username, string password, string urlHost, string url, Request r, Aes aes) - { - string realm = null; - if (r.Realm != null) - realm = CryptoTransform(r.Realm, true, false, aes, CMode.DECRYPT); - - var root = host.Database.RootGroup; - var group = root.FindCreateGroup(KEEPASSHTTP_GROUP_NAME, false); - if (group == null) - { - group = new PwGroup(true, true, KEEPASSHTTP_GROUP_NAME, PwIcon.WorldComputer); - root.AddGroup(group, true); - UpdateUI(null); - } - - string submithost = null; - if (r.SubmitUrl != null) - submithost = GetHost(CryptoTransform(r.SubmitUrl, true, false, aes, CMode.DECRYPT)); - - string baseUrl = url; - // index bigger than https:// <-- this slash - if (baseUrl.LastIndexOf("/") > 9) - { - baseUrl = baseUrl.Substring(0, baseUrl.LastIndexOf("/") + 1); - } - - PwEntry entry = new PwEntry(true, true); - entry.Strings.Set(PwDefs.TitleField, new ProtectedString(false, urlHost)); - entry.Strings.Set(PwDefs.UserNameField, new ProtectedString(false, username)); - entry.Strings.Set(PwDefs.PasswordField, new ProtectedString(true, password)); - entry.Strings.Set(PwDefs.UrlField, new ProtectedString(true, baseUrl)); - - if ((submithost != null && urlHost != submithost) || realm != null) - { - var config = new KeePassHttpEntryConfig(); - if (submithost != null) - config.Allow.Add(submithost); - if (realm != null) - config.Realm = realm; - - var serializer = NewJsonSerializer(); - var writer = new StringWriter(); - serializer.Serialize(writer, config); - entry.Strings.Set(KEEPASSHTTP_NAME, new ProtectedString(false, writer.ToString())); - } - - group.AddEntry(entry, true); - UpdateUI(group); - - return true; - } -} -*/ -} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java index aaee97b1..b90562dd 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java @@ -2,7 +2,6 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.google.gson.stream.JsonWriter; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.AbstractHandler; import org.linguafranca.pwdb.kdbx.Helpers; @@ -24,6 +23,7 @@ public class KeePassHttpHandler extends AbstractHandler { private Logger logger = LoggerFactory.getLogger(KeePassHttpHandler.class); private Gson gson = new GsonBuilder().disableHtmlEscaping().serializeNulls().create(); private Processor processor = new Processor(); + private Crypto crypto = new Crypto(); @Override public void handle(String s, Request request, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException, ServletException { @@ -46,35 +46,32 @@ public void handle(String s, Request request, HttpServletRequest httpServletRequ } Message.Response response = new Message.Response(request1.RequestType, processor.getHash()); - Processor.RequestHandler handler = processor.getHandler(request1.RequestType); - - if (handler == null) { - httpServletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); - request.setHandled(true); - response.Success = false; - response.Error = "No valid request type found " + request1.RequestType; - httpServletResponse.getWriter().write(gson.toJson(response)); - return; - } if (request1.RequestType.equals(Message.Type.ASSOCIATE)) { - processor.getCrypto().setKey(Helpers.decodeBase64Content(request1.Key.getBytes(), false)); + crypto.setKey(Helpers.decodeBase64Content(request1.Key.getBytes(), false)); } - // normal part of the protocol to fail verfication on test-associate - if (!processor.getCrypto().verify(request1)) { + httpServletResponse.setStatus(HttpServletResponse.SC_OK); + + // normal part of the protocol to fail verification on test-associate + if (!crypto.verify(request1)) { logger.debug("Request failed verification"); response.Success = false; response.Error = ""; response.Hash=""; } else { - handler.process(request1, response); - processor.getCrypto().makeVerifiable(response); + try { + processor.process(request1, response); + crypto.makeVerifiable(response); + } catch (Exception e) { + httpServletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.Success = false; + response.Error = "Error processing request " + e.getMessage(); + } } - // create a logging outputstream to see what is being sent + // create a logging outputStream to see what is being sent OutputStream outputStream = new LogginOutputStream(httpServletResponse.getOutputStream(), logger); Writer writer = new BufferedWriter(new OutputStreamWriter(outputStream)); - httpServletResponse.setStatus(HttpServletResponse.SC_OK); gson.toJson(response, writer); writer.flush(); request.setHandled(true); diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java index 5bd0e3c6..d038e762 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java @@ -5,12 +5,22 @@ import java.util.List; /** - * @author jo + * Message definitions for the protocol + *

+ * Note non idiomatic case on field names, this allows correct + * serialization as JSON without annotations */ +@SuppressWarnings("WeakerAccess") public class Message { + /** + * Protocol version + */ public static final String VERSION = "1.8.4.1"; + /** + * The valid message types + */ public static final class Type { public static final String GET_LOGINS = "get-logins"; public static final String GET_LOGINS_COUNT = "get-logins-count"; @@ -22,6 +32,9 @@ public static final class Type { } + /** + * Base class for requests and responses, they can be cryptographically verified + */ public static class Verifiable { /** * Nonce value used in conjunction with all encrypted fields, @@ -35,6 +48,9 @@ public static class Verifiable { public String Verifier=""; } + /** + * From client to server + */ public static class Request extends Verifiable { public String RequestType; @@ -83,6 +99,9 @@ public static class Request extends Verifiable { public String Realm; } + /** + * From server to client + */ public static class Response extends Verifiable { public Response(String request, String hash) { RequestType = request; diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java index 69a15c43..e0421cd6 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java @@ -8,65 +8,45 @@ /** * - * Contains message handlers for + * Contains message processors for * * @author jo */ public class Processor { - - private Map handlers = new HashMap(); - private DatabaseAdaptor adaptor = new DatabaseAdaptor.Default(); - private Crypto crypto = new Crypto(); - - public Processor(String base64Key) { - this(Helpers.decodeBase64Content(base64Key.getBytes(), false)); + private interface MessageProcessor { + void process(Message.Request request, Message.Response response); } - public Processor(byte[] binaryKey) { - this(); - crypto.setKey(binaryKey); - } + private Map processors = new HashMap(); + private DatabaseAdaptor adaptor = new DatabaseAdaptor.Default(); public Processor() { - handlers.put(Message.Type.TEST_ASSOCIATE, new TestAssociateHandler()); - handlers.put(Message.Type.ASSOCIATE, new AssociateHandler()); - handlers.put(Message.Type.GET_LOGINS, new GetLoginsHandler()); -/* - handlers.put(Message.Request.GET_LOGINS_COUNT, GetLoginsCountHandler); - handlers.put(Message.Request.GET_ALL_LOGINS, GetAllLoginsHandler); - handlers.put(Message.Request.SET_LOGIN, SetLoginHandler); - handlers.put(Message.Request.GENERATE_PASSWORD, GeneratePassword); -*/ - } - - public Crypto getCrypto() { - return crypto; + processors.put(Message.Type.TEST_ASSOCIATE, new TestAssociate()); + processors.put(Message.Type.ASSOCIATE, new Associate()); + processors.put(Message.Type.GET_LOGINS, new GetLogins()); + processors.put(Message.Type.GET_LOGINS_COUNT, new GetLoginsCount()); + processors.put(Message.Type.GET_ALL_LOGINS, new GetAllLogins()); + processors.put(Message.Type.SET_LOGIN, new SetLogin()); + processors.put(Message.Type.GENERATE_PASSWORD, new GeneratePassword()); } public String getHash() { return adaptor.getHash(); } - public interface RequestHandler { - void process(Message.Request request, Message.Response response); - } - - public RequestHandler getHandler(String requestType) { - return handlers.get(requestType); + public void process(Message.Request request, Message.Response response) { + processors.get(request.RequestType).process(request, response); } - - private class AssociateHandler implements RequestHandler { + private class Associate implements MessageProcessor { @Override public void process(Message.Request request, Message.Response response) { - Processor.this.crypto.setKey(Helpers.decodeBase64Content(request.Key.getBytes(), false)); - response.Id = adaptor.getId(); response.Success = true; } } - private class TestAssociateHandler implements RequestHandler { + private class TestAssociate implements MessageProcessor { @Override public void process(Message.Request request, Message.Response response) { response.Success = false; @@ -77,7 +57,7 @@ public void process(Message.Request request, Message.Response response) { } } - private class GetLoginsHandler implements Processor.RequestHandler { + private class GetLogins implements MessageProcessor { public void process(Message.Request r, Message.Response resp) { resp.Entries.add(new Message.ResponseEntry("FB", "FBLOGIN", "FBPASS", @@ -87,163 +67,26 @@ public void process(Message.Request r, Message.Response resp) { resp.Id = adaptor.getId(); resp.Success = true; resp.Count = resp.Entries.size(); + } + } -/* - string submithost = null; - var host = GetHost(CryptoTransform(r.Url, true, false, aes, CMode.DECRYPT)); - if (r.SubmitUrl != null) - submithost = GetHost(CryptoTransform(r.SubmitUrl, true, false, aes, CMode.DECRYPT)); - - var items = FindMatchingEntries(r, aes); - if (items.ToList().Count > 0) { - Func filter = delegate(PwEntry e) - { - var c = GetEntryConfig(e); - - var title = e.Strings.ReadSafe(PwDefs.TitleField); - var entryUrl = e.Strings.ReadSafe(PwDefs.UrlField); - if (c != null) { - return title != host && entryUrl != host && !c.Allow.Contains(host) || (submithost != null && !c.Allow.Contains(submithost) && submithost != title && submithost != entryUrl); - } - return title != host && entryUrl != host || (submithost != null && title != submithost && entryUrl != submithost); - } - ; - - var configOpt = new ConfigOpt(this.host.CustomConfig); - var config = GetConfigEntry(true); - var autoAllowS = config.Strings.ReadSafe("Auto Allow"); - var autoAllow = autoAllowS != null && autoAllowS.Trim() != ""; - autoAllow = autoAllow || configOpt.AlwaysAllowAccess; - var needPrompting = from e in items where filter(e.entry) select e; - - if (needPrompting.ToList().Count > 0 && !autoAllow) { - var win = this.host.MainWindow; - - using(var f = new AccessControlForm()) - { - win.Invoke((MethodInvoker) delegate - { - f.Icon = win.Icon; - f.Plugin = this; - f.Entries = (from e in items where filter(e.entry) select e.entry).ToList(); - //f.Entries = needPrompting.ToList(); - f.Host = submithost != null ? submithost : host; - f.Load += delegate { - f.Activate(); - } - ; - f.ShowDialog(win); - if (f.Remember && (f.Allowed || f.Denied)) { - foreach(var e in needPrompting) - { - var c = GetEntryConfig(e.entry); - if (c == null) - c = new KeePassHttpEntryConfig(); - var set = f.Allowed ? c.Allow : c.Deny; - set.Add(host); - if (submithost != null && submithost != host) - set.Add(submithost); - SetEntryConfig(e.entry, c); - - } - } - if (!f.Allowed) { - items = items.Except(needPrompting); - } - }); - } - } - - string compareToUrl = null; - if (r.SubmitUrl != null) { - compareToUrl = CryptoTransform(r.SubmitUrl, true, false, aes, CMode.DECRYPT); - } - if (String.IsNullOrEmpty(compareToUrl)) - compareToUrl = CryptoTransform(r.Url, true, false, aes, CMode.DECRYPT); - - compareToUrl = compareToUrl.ToLower(); - - foreach(var entryDatabase in items) - { - string entryUrl = String.Copy(entryDatabase.entry.Strings.ReadSafe(PwDefs.UrlField)); - if (String.IsNullOrEmpty(entryUrl)) - entryUrl = entryDatabase.entry.Strings.ReadSafe(PwDefs.TitleField); - - entryUrl = entryUrl.ToLower(); - - entryDatabase.entry.UsageCount = (ulong) LevenshteinDistance(compareToUrl, entryUrl); - - } - - var itemsList = items.ToList(); - - if (configOpt.SpecificMatchingOnly) { - itemsList = (from e in itemsList - orderby e.entry.UsageCount ascending - select e).ToList(); - - ulong lowestDistance = itemsList.Count > 0 ? - itemsList[0].entry.UsageCount : - 0; - - itemsList = (from e in itemsList - where e.entry.UsageCount == lowestDistance - orderby e.entry.UsageCount - select e).ToList(); - - } - - if (configOpt.SortResultByUsername) { - var items2 = from e in itemsList orderby e.entry.UsageCount ascending, GetUserPass (e)[0] - ascending select e; - itemsList = items2.ToList(); - } else { - var items2 = from e in itemsList orderby e.entry.UsageCount ascending, e. - entry.Strings.ReadSafe(PwDefs.TitleField) ascending select e; - 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 < string > ()); - - if (configOpt.ReceiveCredentialNotification) - ShowNotification(String.Format("{0}: {1} is receiving credentials for:\n {2}", r.Id, host, n)); - } + private class GeneratePassword implements MessageProcessor { + public void process(Message.Request r, Message.Response resp) { + } + } - 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); - } - } - } + private class GetAllLogins implements MessageProcessor { + public void process(Message.Request r, Message.Response resp) { + } + } - resp.Count = resp.Entries.Count; - } else { - resp.Success = true; - resp.Id = r.Id; - SetResponseVerifier(resp, aes); + private class GetLoginsCount implements MessageProcessor { + public void process(Message.Request r, Message.Response resp) { } -*/ + } + + private class SetLogin implements MessageProcessor { + public void process(Message.Request r, Message.Response resp) { } } diff --git a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java index 9cb79151..c8c05e92 100644 --- a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java +++ b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java @@ -30,18 +30,19 @@ public void sessionTest() { Processor processor = new Processor(); Message.Request request = gson.fromJson(associateRequest, Message.Request.class); + Crypto crypto = new Crypto(request.Key); Message.Response actualResponse = gson.fromJson(associateResponse, Message.Response.class); Message.Response computedResponse = new Message.Response(request.RequestType, "eefc4faf792a59d034da69c69f3643ef34ba7d8d"); - processor.getHandler(request.RequestType).process(request, computedResponse); + processor.process(request, computedResponse); computedResponse.Id = "NEW coNECTION"; - processor.getCrypto().makeVerifiable(computedResponse); + crypto.makeVerifiable(computedResponse); System.out.println(gson.toJson(computedResponse)); - processor.getCrypto().verify(computedResponse); + crypto.verify(computedResponse); System.out.println(gson.toJson(actualResponse)); - processor.getCrypto().verify(actualResponse); + crypto.verify(actualResponse); @@ -49,34 +50,34 @@ public void sessionTest() { actualResponse = gson.fromJson(testAssociateResponseWithId, Message.Response.class); computedResponse = new Message.Response(request.RequestType, "eefc4faf792a59d034da69c69f3643ef34ba7d8d"); - processor.getHandler(request.RequestType).process(request, computedResponse); - processor.getCrypto().makeVerifiable(computedResponse); + processor.process(request, computedResponse); + crypto.makeVerifiable(computedResponse); computedResponse.Id = "NEW coNECTION"; System.out.println(gson.toJson(computedResponse)); - processor.getCrypto().verify(computedResponse); + crypto.verify(computedResponse); System.out.println(gson.toJson(actualResponse)); - processor.getCrypto().verify(actualResponse); + crypto.verify(actualResponse); request = gson.fromJson(getLoginsRequest, Message.Request.class); actualResponse = gson.fromJson(getLoginsResponse, Message.Response.class); computedResponse = new Message.Response(request.RequestType, "eefc4faf792a59d034da69c69f3643ef34ba7d8d"); - processor.getHandler(request.RequestType).process(request, computedResponse); - processor.getCrypto().makeVerifiable(computedResponse); + processor.process(request, computedResponse); + crypto.makeVerifiable(computedResponse); computedResponse.Id = "NEW coNECTION"; System.out.println(gson.toJson(computedResponse)); - processor.getCrypto().verify(computedResponse); + crypto.verify(computedResponse); System.out.println(gson.toJson(actualResponse)); - processor.getCrypto().verify(actualResponse); + crypto.verify(actualResponse); byte[] iv = Helpers.decodeBase64Content(actualResponse.Nonce.getBytes(), false); for (Message.ResponseEntry entry: actualResponse.Entries) { - System.out.println(processor.getCrypto().decryptFromBase64(entry.Login, iv)); - System.out.println(processor.getCrypto().decryptFromBase64(entry.Name, iv)); - System.out.println(processor.getCrypto().decryptFromBase64(entry.Password, iv)); - System.out.println(processor.getCrypto().decryptFromBase64(entry.Uuid, iv)); + System.out.println(crypto.decryptFromBase64(entry.Login, iv)); + System.out.println(crypto.decryptFromBase64(entry.Name, iv)); + System.out.println(crypto.decryptFromBase64(entry.Password, iv)); + System.out.println(crypto.decryptFromBase64(entry.Uuid, iv)); } diff --git a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java index 8f447935..00b5a034 100644 --- a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java +++ b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java @@ -32,49 +32,52 @@ public class TestRequestResponse { @Test public void testVerifyAssociate() { Message.Request r = gson.fromJson(associate, Message.Request.class); - Processor processor = new Processor(r.Key); - assertArrayEquals(r.Key.getBytes(), Helpers.encodeBase64Content(processor.getCrypto().getKey()).getBytes()); + Processor processor = new Processor(); + Crypto crypto = new Crypto(r.Key); + assertArrayEquals(r.Key.getBytes(), Helpers.encodeBase64Content(crypto.getKey()).getBytes()); byte[] iv = new SecureRandom().generateSeed(16); - String secret = processor.getCrypto().encryptToBase64("Secret", iv); - assertEquals("Secret", processor.getCrypto().decryptFromBase64(secret, iv)); + String secret = crypto.encryptToBase64("Secret", iv); + assertEquals("Secret", crypto.decryptFromBase64(secret, iv)); - assertTrue(processor.getCrypto().verify(r)); - assertTrue(processor.getCrypto().verify(gson.fromJson(response, Message.Response.class))); - assertTrue(processor.getCrypto().verify(gson.fromJson(reTest, Message.Request.class))); - assertTrue(processor.getCrypto().verify(gson.fromJson(reResponse, Message.Response.class))); + assertTrue(crypto.verify(r)); + assertTrue(crypto.verify(gson.fromJson(response, Message.Response.class))); + assertTrue(crypto.verify(gson.fromJson(reTest, Message.Request.class))); + assertTrue(crypto.verify(gson.fromJson(reResponse, Message.Response.class))); } @Test public void testVerifyAssociateRequestResponse() { Message.Request r = gson.fromJson(requestString, Message.Request.class); - Processor processor = new Processor(r.Key); - assertTrue(processor.getCrypto().verify(r)); + Processor processor = new Processor(); + Crypto crypto = new Crypto(r.Key); + assertTrue(crypto.verify(r)); Message.Response response = new Message.Response(r.RequestType,""); - processor.getCrypto().makeVerifiable(response); + crypto.makeVerifiable(response); - assertTrue(processor.getCrypto().verify(response)); + assertTrue(crypto.verify(response)); } @Test public void testGetLogins() { Message.Request r = gson.fromJson(getLoginsAssociate, Message.Request.class); - Processor processor = new Processor(r.Key); - processor.getCrypto().verify(r); + Processor processor = new Processor(); + Crypto crypto = new Crypto(r.Key); + crypto.verify(r); Message.Request l = gson.fromJson(getLogins, Message.Request.class); - assertTrue(processor.getCrypto().verify(l)); + assertTrue(crypto.verify(l)); String encodedUrl = l.Url; - PaddedBufferedBlockCipher cipher = processor.getCrypto().getCipher(Crypto.CMode.DECRYPT, Helpers.decodeBase64Content(l.Nonce.getBytes(), false)); - String unencodedUrl = processor.getCrypto().CryptoTransform(encodedUrl, true, false, cipher); + PaddedBufferedBlockCipher cipher = crypto.getCipher(Crypto.CMode.DECRYPT, Helpers.decodeBase64Content(l.Nonce.getBytes(), false)); + String unencodedUrl = crypto.CryptoTransform(encodedUrl, true, false, cipher); System.out.println(unencodedUrl); assertEquals("https://www.facebook.com", unencodedUrl); cipher.reset(); String encodedSubmitUrl = l.SubmitUrl; - String unencodedSubmitUrl = processor.getCrypto().CryptoTransform(encodedSubmitUrl,true,false, cipher); + String unencodedSubmitUrl = crypto.CryptoTransform(encodedSubmitUrl,true,false, cipher); System.out.println(unencodedSubmitUrl); assertEquals("https://www.facebook.com/login.php?login_attempt=1&lwv=110", unencodedSubmitUrl); @@ -82,9 +85,9 @@ public void testGetLogins() { response.Success=true; response.Count=1; response.Entries.add(new Message.ResponseEntry("a", "b", "c","uuid", new ArrayList())); - processor.getCrypto().makeVerifiable(response); + crypto.makeVerifiable(response); - assertTrue(processor.getCrypto().verify(response)); + assertTrue(crypto.verify(response)); } From efa0feb346c490d3084f87027b4f6e8bc281265e Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 24 Jan 2017 19:08:06 +0000 Subject: [PATCH 020/228] Still more tidying up --- .../pwdb/keepasshttp/DatabaseAdaptor.java | 6 ++++- .../pwdb/keepasshttp/KeePassHttpHandler.java | 11 ++++++-- .../pwdb/keepasshttp/KeePassHttpServer.java | 3 ++- .../pwdb/keepasshttp/Processor.java | 26 ++++++++++--------- .../keepasshttp/MonitoredRequestsTest.java | 7 ++--- .../pwdb/keepasshttp/TestRequestResponse.java | 10 +++---- 6 files changed, 36 insertions(+), 27 deletions(-) diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java index 70a03286..e845f5ed 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java @@ -22,7 +22,11 @@ class Default implements DatabaseAdaptor { private String id = "402881E9-58B6-5A30-0158-B65A30B20000"; private UUID rootGroupUuid = UUID.fromString("402881E9-58B6-5A30-0158-B65AFC580001"); private UUID recycleBinUuid = UUID.fromString("402881E9-58B6-5A30-0158-B65BC8D30002"); - private Database database = new SimpleDatabase(); + private Database database; + + Default(Database database) { + this.database = database; + } @Override public String getId() { diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java index b90562dd..06f67ab8 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java @@ -4,6 +4,7 @@ import com.google.gson.GsonBuilder; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.AbstractHandler; +import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.keepasshttp.util.LogginInputStream; import org.linguafranca.pwdb.keepasshttp.util.LogginOutputStream; @@ -20,11 +21,17 @@ */ public class KeePassHttpHandler extends AbstractHandler { + private final DatabaseAdaptor adaptor; + private Processor processor; private Logger logger = LoggerFactory.getLogger(KeePassHttpHandler.class); private Gson gson = new GsonBuilder().disableHtmlEscaping().serializeNulls().create(); - private Processor processor = new Processor(); private Crypto crypto = new Crypto(); + KeePassHttpHandler(Database database) { + this.adaptor = new DatabaseAdaptor.Default(database); + this.processor = new Processor(database); + } + @Override public void handle(String s, Request request, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException, ServletException { @@ -45,7 +52,7 @@ public void handle(String s, Request request, HttpServletRequest httpServletRequ return; } - Message.Response response = new Message.Response(request1.RequestType, processor.getHash()); + Message.Response response = new Message.Response(request1.RequestType, adaptor.getHash()); if (request1.RequestType.equals(Message.Type.ASSOCIATE)) { crypto.setKey(Helpers.decodeBase64Content(request1.Key.getBytes(), false)); diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpServer.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpServer.java index 7f6c1e9a..9805ff11 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpServer.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpServer.java @@ -2,6 +2,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; /** * Embedded server @@ -14,7 +15,7 @@ public static void main(String[] args) throws Exception { http.setPort(19455); http.setIdleTimeout(300000); server.addConnector(http); - server.setHandler(new KeePassHttpHandler()); + server.setHandler(new KeePassHttpHandler(new SimpleDatabase())); server.start(); server.join(); } diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java index e0421cd6..5f549375 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java @@ -1,5 +1,6 @@ package org.linguafranca.pwdb.keepasshttp; +import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.kdbx.Helpers; import java.util.HashMap; @@ -13,14 +14,12 @@ * @author jo */ public class Processor { - private interface MessageProcessor { - void process(Message.Request request, Message.Response response); - } - + private final Database database; private Map processors = new HashMap(); - private DatabaseAdaptor adaptor = new DatabaseAdaptor.Default(); - public Processor() { + + public Processor(Database database) { + this.database = database; processors.put(Message.Type.TEST_ASSOCIATE, new TestAssociate()); processors.put(Message.Type.ASSOCIATE, new Associate()); processors.put(Message.Type.GET_LOGINS, new GetLogins()); @@ -30,18 +29,21 @@ public Processor() { processors.put(Message.Type.GENERATE_PASSWORD, new GeneratePassword()); } - public String getHash() { - return adaptor.getHash(); + private interface MessageProcessor { + void process(Message.Request request, Message.Response response); } public void process(Message.Request request, Message.Response response) { processors.get(request.RequestType).process(request, response); } + private String makeId() { + return database.getName() + " (" + database.getRootGroup().getUuid().toString() + ")"; + } private class Associate implements MessageProcessor { @Override public void process(Message.Request request, Message.Response response) { - response.Id = adaptor.getId(); + response.Id = makeId(); response.Success = true; } } @@ -51,8 +53,8 @@ private class TestAssociate implements MessageProcessor { public void process(Message.Request request, Message.Response response) { response.Success = false; if (request.Id != null) { - response.Success = request.Id.equals(adaptor.getId()); - response.Id = adaptor.getId(); + response.Success = request.Id.equals(makeId()); + response.Id = makeId(); } } } @@ -64,7 +66,7 @@ public void process(Message.Request r, Message.Response resp) { Helpers.base64FromUuid(UUID.randomUUID()))); resp.Entries.add(new Message.ResponseEntry("FB2", "FBLOGIN2", "FBPASS2", Helpers.base64FromUuid(UUID.randomUUID()))); - resp.Id = adaptor.getId(); + resp.Id = makeId(); resp.Success = true; resp.Count = resp.Entries.size(); } diff --git a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java index c8c05e92..e8b56fb3 100644 --- a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java +++ b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java @@ -4,10 +4,7 @@ import com.google.gson.GsonBuilder; import org.junit.Test; import org.linguafranca.pwdb.kdbx.Helpers; -import org.linguafranca.pwdb.keepasshttp.Message; -import org.linguafranca.pwdb.keepasshttp.Processor; - -import static org.junit.Assert.assertEquals; +import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; /** * @author jo @@ -27,7 +24,7 @@ public class MonitoredRequestsTest { @Test public void sessionTest() { - Processor processor = new Processor(); + Processor processor = new Processor(new SimpleDatabase()); Message.Request request = gson.fromJson(associateRequest, Message.Request.class); Crypto crypto = new Crypto(request.Key); diff --git a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java index 00b5a034..8ec70f9a 100644 --- a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java +++ b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java @@ -4,6 +4,7 @@ import com.google.gson.GsonBuilder; import org.junit.Test; import org.linguafranca.pwdb.kdbx.Helpers; +import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; import org.linguafranca.pwdb.keepasshttp.Crypto; import org.linguafranca.pwdb.keepasshttp.Processor; import org.linguafranca.pwdb.keepasshttp.Message; @@ -32,7 +33,6 @@ public class TestRequestResponse { @Test public void testVerifyAssociate() { Message.Request r = gson.fromJson(associate, Message.Request.class); - Processor processor = new Processor(); Crypto crypto = new Crypto(r.Key); assertArrayEquals(r.Key.getBytes(), Helpers.encodeBase64Content(crypto.getKey()).getBytes()); @@ -50,7 +50,6 @@ public void testVerifyAssociate() { @Test public void testVerifyAssociateRequestResponse() { Message.Request r = gson.fromJson(requestString, Message.Request.class); - Processor processor = new Processor(); Crypto crypto = new Crypto(r.Key); assertTrue(crypto.verify(r)); @@ -63,7 +62,6 @@ public void testVerifyAssociateRequestResponse() { @Test public void testGetLogins() { Message.Request r = gson.fromJson(getLoginsAssociate, Message.Request.class); - Processor processor = new Processor(); Crypto crypto = new Crypto(r.Key); crypto.verify(r); @@ -71,17 +69,17 @@ public void testGetLogins() { assertTrue(crypto.verify(l)); String encodedUrl = l.Url; PaddedBufferedBlockCipher cipher = crypto.getCipher(Crypto.CMode.DECRYPT, Helpers.decodeBase64Content(l.Nonce.getBytes(), false)); - String unencodedUrl = crypto.CryptoTransform(encodedUrl, true, false, cipher); + String unencodedUrl = Crypto.CryptoTransform(encodedUrl, true, false, cipher); System.out.println(unencodedUrl); assertEquals("https://www.facebook.com", unencodedUrl); cipher.reset(); String encodedSubmitUrl = l.SubmitUrl; - String unencodedSubmitUrl = crypto.CryptoTransform(encodedSubmitUrl,true,false, cipher); + String unencodedSubmitUrl = Crypto.CryptoTransform(encodedSubmitUrl,true,false, cipher); System.out.println(unencodedSubmitUrl); assertEquals("https://www.facebook.com/login.php?login_attempt=1&lwv=110", unencodedSubmitUrl); - Message.Response response = new Message.Response(l.RequestType, processor.getHash()); + Message.Response response = new Message.Response(l.RequestType, new DatabaseAdaptor.Default(new SimpleDatabase()).getHash()); response.Success=true; response.Count=1; response.Entries.add(new Message.ResponseEntry("a", "b", "c","uuid", new ArrayList())); From b69db97da04abce4eab706f91153263ea915e9cb Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 24 Jan 2017 21:49:30 +0000 Subject: [PATCH 021/228] Implement password generator saving and retrieval of credentials --- .../linguafranca/pwdb/keepasshttp/Crypto.java | 55 ++++++-- .../pwdb/keepasshttp/KeePassHttpHandler.java | 5 +- .../pwdb/keepasshttp/KeePassHttpServer.java | 18 ++- .../pwdb/keepasshttp/Processor.java | 123 ++++++++++++------ .../pwdb/keepasshttp/PwGenerator.java | 8 ++ .../keepasshttp/MonitoredRequestsTest.java | 7 +- 6 files changed, 163 insertions(+), 53 deletions(-) create mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/PwGenerator.java diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java index f3ab44d3..ac6ad90d 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java @@ -17,9 +17,10 @@ class Crypto { private byte[] binaryKey; - Crypto () {} + Crypto() { + } - Crypto (String key) { + Crypto(String key) { this.binaryKey = Helpers.decodeBase64Content(key.getBytes()); } @@ -48,11 +49,12 @@ public boolean getEncrypt() { /** * Return true if the Nonce and the Verifier on a message match + * * @param verifiable a message containing those fields */ boolean verify(Message.Verifiable verifiable) { if (getKey() == null || verifiable.Verifier == null || verifiable.Nonce == null || - verifiable.Verifier.equals("") || verifiable.Nonce.equals("")) { + verifiable.Verifier.equals("") || verifiable.Nonce.equals("")) { return false; } // The nonce is base64 encoded version of an iv @@ -63,8 +65,35 @@ boolean verify(Message.Verifiable verifiable) { return decrypted.equals(verifiable.Nonce); } + boolean verify(Message.Request request) { + if (!verify((Message.Verifiable) request)) { + return false; + } + + byte[] iv = Helpers.decodeBase64Content(request.Nonce.getBytes(), false); + // decrypt all the fields + if (request.Login != null) { + request.Login = decryptFromBase64(request.Login, iv); + } + if (request.Password != null) { + request.Password = decryptFromBase64(request.Password, iv); + } + if (request.Url != null) { + request.Url = decryptFromBase64(request.Url, iv); + } + if (request.SubmitUrl != null) { + request.SubmitUrl = decryptFromBase64(request.SubmitUrl, iv); + } + if (request.Uuid != null) { + request.Uuid = decryptFromBase64(request.Uuid, iv); + } + + return true; + } + /** * Add a Nonce and a Verifier to a message to make it verifiable + * * @param response a message to make verifiable */ void makeVerifiable(Message.Response response) { @@ -92,8 +121,9 @@ void makeVerifiable(Message.Response response) { /** * Get a cipher + * * @param mode encryption or decryption - * @param iv a 16 byte iv + * @param iv a 16 byte iv * @return an initialised Cipher */ PaddedBufferedBlockCipher getCipher(CMode mode, byte[] iv) { @@ -104,30 +134,33 @@ PaddedBufferedBlockCipher getCipher(CMode mode, byte[] iv) { /** * Return an unencrypted non encoded copy of an encrypted base 64 encoded string + * * @param input cipher text - * @param iv an iv + * @param iv an iv * @return plain text */ - String decryptFromBase64(String input, byte[] iv){ + String decryptFromBase64(String input, byte[] iv) { return CryptoTransform(input, true, false, getCipher(CMode.DECRYPT, iv)); } /** * Return an encrypted base 64 encoded copy of plain text string + * * @param input plain text - * @param iv an iv + * @param iv an iv * @return cipher text */ - String encryptToBase64(String input, byte[] iv){ + String encryptToBase64(String input, byte[] iv) { return CryptoTransform(input, false, true, getCipher(CMode.ENCRYPT, iv)); } /** * Encryption and Decryption Helper - * @param input the candidate for transformation - * @param base64in true if base 64 encoded + * + * @param input the candidate for transformation + * @param base64in true if base 64 encoded * @param base64out true if we require base 64 out - * @param cipher a Cipher initialised for Encrypt or Decrypt + * @param cipher a Cipher initialised for Encrypt or Decrypt * @return the transformed result */ static String CryptoTransform(String input, boolean base64in, boolean base64out, PaddedBufferedBlockCipher cipher) { diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java index 06f67ab8..7d253583 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java @@ -27,9 +27,9 @@ public class KeePassHttpHandler extends AbstractHandler { private Gson gson = new GsonBuilder().disableHtmlEscaping().serializeNulls().create(); private Crypto crypto = new Crypto(); - KeePassHttpHandler(Database database) { + KeePassHttpHandler(Database database, PwGenerator pwGenerator) { this.adaptor = new DatabaseAdaptor.Default(database); - this.processor = new Processor(database); + this.processor = new Processor(database, pwGenerator); } @Override @@ -65,7 +65,6 @@ public void handle(String s, Request request, HttpServletRequest httpServletRequ logger.debug("Request failed verification"); response.Success = false; response.Error = ""; - response.Hash=""; } else { try { processor.process(request1, response); diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpServer.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpServer.java index 9805ff11..6d90dda2 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpServer.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpServer.java @@ -4,6 +4,9 @@ import org.eclipse.jetty.server.ServerConnector; import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; +import java.security.SecureRandom; +import java.util.Random; + /** * Embedded server */ @@ -15,7 +18,20 @@ public static void main(String[] args) throws Exception { http.setPort(19455); http.setIdleTimeout(300000); server.addConnector(http); - server.setHandler(new KeePassHttpHandler(new SimpleDatabase())); + server.setHandler(new KeePassHttpHandler(new SimpleDatabase(), new PwGenerator() { + @Override + public String generate() { + String[] symbols = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"}; + int length = 10; + Random random = new SecureRandom(); + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + int indexRandom = random.nextInt( symbols.length ); + sb.append( symbols[indexRandom] ); + } + return sb.toString(); + } + })); server.start(); server.join(); } diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java index 5f549375..c5759948 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java @@ -1,27 +1,52 @@ package org.linguafranca.pwdb.keepasshttp; import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.Entry; import org.linguafranca.pwdb.kdbx.Helpers; +import org.linguafranca.pwdb.keepasshttp.Message.ResponseEntry; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; /** * - * Contains message processors for + * Contains message processors for processing messages (doh) * * @author jo */ public class Processor { + + private interface MessageProcessor { + void process(Message.Request request, Message.Response response); + } + private final Database database; + private final PwGenerator pwGenerator; private Map processors = new HashMap(); - public Processor(Database database) { + public Processor(Database database, PwGenerator pwGenerator) { this.database = database; - processors.put(Message.Type.TEST_ASSOCIATE, new TestAssociate()); - processors.put(Message.Type.ASSOCIATE, new Associate()); + this.pwGenerator = pwGenerator; + processors.put(Message.Type.TEST_ASSOCIATE, new MessageProcessor() { + @Override + public void process(Message.Request request, Message.Response response) { + response.Success = false; + if (request.Id != null) { + response.Success = request.Id.equals(makeId()); + response.Id = makeId(); + } + } + }); + processors.put(Message.Type.ASSOCIATE, new MessageProcessor() { + @Override + public void process(Message.Request request, Message.Response response) { + response.Id = makeId(); + response.Success = true; + } + }); processors.put(Message.Type.GET_LOGINS, new GetLogins()); processors.put(Message.Type.GET_LOGINS_COUNT, new GetLoginsCount()); processors.put(Message.Type.GET_ALL_LOGINS, new GetAllLogins()); @@ -29,10 +54,6 @@ public Processor(Database database) { processors.put(Message.Type.GENERATE_PASSWORD, new GeneratePassword()); } - private interface MessageProcessor { - void process(Message.Request request, Message.Response response); - } - public void process(Message.Request request, Message.Response response) { processors.get(request.RequestType).process(request, response); } @@ -40,55 +61,83 @@ public void process(Message.Request request, Message.Response response) { private String makeId() { return database.getName() + " (" + database.getRootGroup().getUuid().toString() + ")"; } - private class Associate implements MessageProcessor { - @Override - public void process(Message.Request request, Message.Response response) { - response.Id = makeId(); - response.Success = true; - } - } - - private class TestAssociate implements MessageProcessor { - @Override - public void process(Message.Request request, Message.Response response) { - response.Success = false; - if (request.Id != null) { - response.Success = request.Id.equals(makeId()); - response.Id = makeId(); - } - } - } private class GetLogins implements MessageProcessor { - public void process(Message.Request r, Message.Response resp) { - - resp.Entries.add(new Message.ResponseEntry("FB", "FBLOGIN", "FBPASS", - Helpers.base64FromUuid(UUID.randomUUID()))); - resp.Entries.add(new Message.ResponseEntry("FB2", "FBLOGIN2", "FBPASS2", - Helpers.base64FromUuid(UUID.randomUUID()))); + public void process(final Message.Request r, Message.Response resp) { + + @SuppressWarnings("unchecked") + List entries = database.findEntries(new Entry.Matcher() { + @Override + public boolean matches(Entry entry) { + return entry.getUrl().startsWith(r.Url) || r.Url.startsWith(entry.getUrl()); + } + }); + + for (Entry entry: entries) { + resp.Entries.add(new ResponseEntry(entry.getTitle(), entry.getUsername(), entry.getPassword(), entry.getUuid().toString())); + } + resp.Count = resp.Entries.size(); resp.Id = makeId(); resp.Success = true; - resp.Count = resp.Entries.size(); } } - private class GeneratePassword implements MessageProcessor { + private class GetLoginsCount implements MessageProcessor { public void process(Message.Request r, Message.Response resp) { + processors.get(Message.Type.GET_LOGINS).process(r, resp); + resp.Entries = null; } } - private class GetAllLogins implements MessageProcessor { + private class GeneratePassword implements MessageProcessor { public void process(Message.Request r, Message.Response resp) { + String p = pwGenerator.generate(); + resp.Entries.add(new ResponseEntry("Password", "login", p, UUID.randomUUID().toString())); + resp.Count = resp.Entries.size(); + resp.Id = makeId(); + resp.Success = true; } } - private class GetLoginsCount implements MessageProcessor { + private class GetAllLogins implements MessageProcessor { public void process(Message.Request r, Message.Response resp) { + @SuppressWarnings("unchecked") + List entries = database.findEntries(new Entry.Matcher() { + @Override + public boolean matches(Entry entry) { + return false; //entry.getUrl().equals(url); + } + }); + resp.Count = resp.Entries.size(); + resp.Id = makeId(); + resp.Success = true; } } private class SetLogin implements MessageProcessor { - public void process(Message.Request r, Message.Response resp) { + public void process(final Message.Request r, Message.Response resp) { + Entry entry = null; + if (r.Uuid != null) { + List entries = database.findEntries(new Entry.Matcher() { + @Override + public boolean matches(Entry entry) { + return entry.getUuid().toString().equals(r.Uuid); + } + }); + entry = (Entry) entries.get(0); + } + if (entry == null) { + entry = database.newEntry(); + } + entry.setTitle("New Entry"); + entry.setPassword(r.Password); + entry.setUsername(r.Login); + entry.setUrl(r.Url); + entry.setProperty("SubmitUrl", r.SubmitUrl); + entry.setNotes("Created automatically"); + database.getRootGroup().addEntry(entry); + resp.Id = makeId(); + resp.Success = true; } } diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/PwGenerator.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/PwGenerator.java new file mode 100644 index 00000000..9b061402 --- /dev/null +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/PwGenerator.java @@ -0,0 +1,8 @@ +package org.linguafranca.pwdb.keepasshttp; + +/** + * @author jo + */ +public interface PwGenerator { + String generate(); +} diff --git a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java index e8b56fb3..e3a19702 100644 --- a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java +++ b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java @@ -24,7 +24,12 @@ public class MonitoredRequestsTest { @Test public void sessionTest() { - Processor processor = new Processor(new SimpleDatabase()); + Processor processor = new Processor(new SimpleDatabase(), new PwGenerator() { + @Override + public String generate() { + return "123"; + } + }); Message.Request request = gson.fromJson(associateRequest, Message.Request.class); Crypto crypto = new Crypto(request.Key); From fc875d7c50e38cb97baec7f566e270c93370eb3e Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 24 Jan 2017 22:04:00 +0000 Subject: [PATCH 022/228] Tidies --- .../pwdb/keepasshttp/Processor.java | 67 ++++++++++--------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java index c5759948..5b691abc 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java @@ -2,21 +2,17 @@ import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.Entry; -import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.keepasshttp.Message.ResponseEntry; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.text.SimpleDateFormat; +import java.util.*; /** - * * Contains message processors for processing messages (doh) * * @author jo */ -public class Processor { +class Processor { private interface MessageProcessor { void process(Message.Request request, Message.Response response); @@ -24,29 +20,14 @@ private interface MessageProcessor { private final Database database; private final PwGenerator pwGenerator; - private Map processors = new HashMap(); + private Map processors = new HashMap<>(); - public Processor(Database database, PwGenerator pwGenerator) { + Processor(Database database, PwGenerator pwGenerator) { this.database = database; this.pwGenerator = pwGenerator; - processors.put(Message.Type.TEST_ASSOCIATE, new MessageProcessor() { - @Override - public void process(Message.Request request, Message.Response response) { - response.Success = false; - if (request.Id != null) { - response.Success = request.Id.equals(makeId()); - response.Id = makeId(); - } - } - }); - processors.put(Message.Type.ASSOCIATE, new MessageProcessor() { - @Override - public void process(Message.Request request, Message.Response response) { - response.Id = makeId(); - response.Success = true; - } - }); + processors.put(Message.Type.TEST_ASSOCIATE, new TestAssociate()); + processors.put(Message.Type.ASSOCIATE, new Associate()); processors.put(Message.Type.GET_LOGINS, new GetLogins()); processors.put(Message.Type.GET_LOGINS_COUNT, new GetLoginsCount()); processors.put(Message.Type.GET_ALL_LOGINS, new GetAllLogins()); @@ -54,7 +35,7 @@ public void process(Message.Request request, Message.Response response) { processors.put(Message.Type.GENERATE_PASSWORD, new GeneratePassword()); } - public void process(Message.Request request, Message.Response response) { + void process(Message.Request request, Message.Response response) { processors.get(request.RequestType).process(request, response); } @@ -73,7 +54,7 @@ public boolean matches(Entry entry) { } }); - for (Entry entry: entries) { + for (Entry entry : entries) { resp.Entries.add(new ResponseEntry(entry.getTitle(), entry.getUsername(), entry.getPassword(), entry.getUuid().toString())); } resp.Count = resp.Entries.size(); @@ -105,15 +86,20 @@ public void process(Message.Request r, Message.Response resp) { List entries = database.findEntries(new Entry.Matcher() { @Override public boolean matches(Entry entry) { - return false; //entry.getUrl().equals(url); + return true; } }); + for (Entry entry : entries) { + resp.Entries.add(new ResponseEntry(entry.getTitle(), entry.getUsername(), entry.getPassword(), entry.getUuid().toString())); + } resp.Count = resp.Entries.size(); resp.Id = makeId(); resp.Success = true; } } + private static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + private class SetLogin implements MessageProcessor { public void process(final Message.Request r, Message.Response resp) { Entry entry = null; @@ -128,17 +114,38 @@ public boolean matches(Entry entry) { } if (entry == null) { entry = database.newEntry(); + entry.setTitle("New Entry " + format.format(new Date())); } - entry.setTitle("New Entry"); entry.setPassword(r.Password); entry.setUsername(r.Login); entry.setUrl(r.Url); entry.setProperty("SubmitUrl", r.SubmitUrl); entry.setNotes("Created automatically"); + //noinspection unchecked database.getRootGroup().addEntry(entry); resp.Id = makeId(); resp.Success = true; } } + private class Associate implements MessageProcessor { + @Override + public void process(Message.Request request, Message.Response response) { + response.Id = makeId(); + response.Success = true; + } + + } + + private class TestAssociate implements MessageProcessor { + @Override + public void process(Message.Request request, Message.Response response) { + response.Success = false; + if (request.Id != null) { + response.Success = request.Id.equals(makeId()); + response.Id = makeId(); + } + } + + } } From 9c2ae0d15685a64a1ed7011b4fefd826256b4725 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Wed, 25 Jan 2017 10:06:41 +0000 Subject: [PATCH 023/228] Add saving of Database on Change --- .../pwdb/keepasshttp/DatabaseAdaptor.java | 41 +++++++++++++++---- .../pwdb/keepasshttp/KeePassHttpHandler.java | 13 +++--- .../pwdb/keepasshttp/KeePassHttpServer.java | 37 ++++++++++------- .../pwdb/keepasshttp/Message.java | 10 ++--- .../pwdb/keepasshttp/Processor.java | 32 ++++++++------- .../pwdb/keepasshttp/PwGenerator.java | 25 +++++++++++ .../keepasshttp/MonitoredRequestsTest.java | 40 ++++++++++++++---- .../pwdb/keepasshttp/TestRequestResponse.java | 30 +++++++++++++- 8 files changed, 168 insertions(+), 60 deletions(-) diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java index e845f5ed..69d6b5c4 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java @@ -1,12 +1,14 @@ package org.linguafranca.pwdb.keepasshttp; import org.apache.commons.codec.binary.Hex; +import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; import org.spongycastle.crypto.digests.SHA1Digest; +import java.io.*; import java.util.UUID; /** @@ -17,25 +19,31 @@ public interface DatabaseAdaptor { String getId(); String getHash(); Database getDatabase(); + PwGenerator getPwGenerator(); + OutputStream getOutputStream(); + Credentials getCredentials(); class Default implements DatabaseAdaptor { - private String id = "402881E9-58B6-5A30-0158-B65A30B20000"; - private UUID rootGroupUuid = UUID.fromString("402881E9-58B6-5A30-0158-B65AFC580001"); - private UUID recycleBinUuid = UUID.fromString("402881E9-58B6-5A30-0158-B65BC8D30002"); private Database database; + private PwGenerator pwGenerator; + private File databaseFile; + private final Credentials credentials; - Default(Database database) { - this.database = database; + Default(File file, Credentials credentials, PwGenerator pwGenerator) throws Exception { + this.databaseFile = file; + this.pwGenerator = pwGenerator; + this.credentials = credentials; + this.database = SimpleDatabase.load(credentials, new FileInputStream(file)); } @Override public String getId() { - return id; + return database.getName() + " (" + database.getRootGroup().getUuid().toString() + ")"; } @Override public String getHash() { - byte[] toHash = (Helpers.hexStringFromUuid(rootGroupUuid) + Helpers.hexStringFromUuid(recycleBinUuid)).getBytes(); + byte[] toHash = Helpers.hexStringFromUuid(database.getRootGroup().getUuid()).getBytes(); SHA1Digest digest = new SHA1Digest(); byte[] digestBytes = new byte[digest.getDigestSize()]; digest.update(toHash, 0, toHash.length); @@ -48,5 +56,24 @@ public String getHash() { public Database getDatabase() { return database; } + + @Override + public PwGenerator getPwGenerator() { + return pwGenerator; + } + + @Override + public OutputStream getOutputStream() { + try { + return new FileOutputStream(databaseFile); + } catch (FileNotFoundException e) { + throw new IllegalStateException(e); + } + } + + @Override + public Credentials getCredentials() { + return credentials; + } } } diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java index 7d253583..7b23f18f 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java @@ -22,14 +22,14 @@ public class KeePassHttpHandler extends AbstractHandler { private final DatabaseAdaptor adaptor; - private Processor processor; + private final Processor processor; private Logger logger = LoggerFactory.getLogger(KeePassHttpHandler.class); - private Gson gson = new GsonBuilder().disableHtmlEscaping().serializeNulls().create(); + private Gson gson = new GsonBuilder().disableHtmlEscaping().create(); private Crypto crypto = new Crypto(); - KeePassHttpHandler(Database database, PwGenerator pwGenerator) { - this.adaptor = new DatabaseAdaptor.Default(database); - this.processor = new Processor(database, pwGenerator); + KeePassHttpHandler(DatabaseAdaptor adaptor) { + this.adaptor = adaptor; + this.processor = new Processor(adaptor); } @Override @@ -64,11 +64,11 @@ public void handle(String s, Request request, HttpServletRequest httpServletRequ if (!crypto.verify(request1)) { logger.debug("Request failed verification"); response.Success = false; - response.Error = ""; } else { try { processor.process(request1, response); crypto.makeVerifiable(response); + response.Id = adaptor.getId(); } catch (Exception e) { httpServletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.Success = false; @@ -82,5 +82,4 @@ public void handle(String s, Request request, HttpServletRequest httpServletRequ writer.flush(); request.setHandled(true); } - } diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpServer.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpServer.java index 6d90dda2..2f945bb0 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpServer.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpServer.java @@ -2,8 +2,14 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; +import org.linguafranca.pwdb.keepasshttp.PwGenerator.HexPwGenerator; +import java.io.File; +import java.io.FileOutputStream; +import java.nio.file.Files; +import java.nio.file.Paths; import java.security.SecureRandom; import java.util.Random; @@ -11,28 +17,29 @@ * Embedded server */ public class KeePassHttpServer { + private static final String DEFAULT_DB_FILE = "HttpDatabase.kdbx"; public static void main(String[] args) throws Exception { + Server server = new Server(); + ServerConnector http = new ServerConnector(server); http.setHost("0.0.0.0"); http.setPort(19455); http.setIdleTimeout(300000); server.addConnector(http); - server.setHandler(new KeePassHttpHandler(new SimpleDatabase(), new PwGenerator() { - @Override - public String generate() { - String[] symbols = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"}; - int length = 10; - Random random = new SecureRandom(); - StringBuilder sb = new StringBuilder(length); - for (int i = 0; i < length; i++) { - int indexRandom = random.nextInt( symbols.length ); - sb.append( symbols[indexRandom] ); - } - return sb.toString(); - } - })); + + KdbxCreds creds = new KdbxCreds("123".getBytes()); + // create a database if we don't have one already + if (Files.notExists(Paths.get(DEFAULT_DB_FILE))) { + SimpleDatabase db = new SimpleDatabase(); + db.setName("HTTP Database"); + db.save(creds,new FileOutputStream(DEFAULT_DB_FILE)); + } + + DatabaseAdaptor adaptor = new DatabaseAdaptor.Default(new File(DEFAULT_DB_FILE), creds, new HexPwGenerator(10)); + server.setHandler(new KeePassHttpHandler(adaptor)); + server.start(); server.join(); } -} + } diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java index d038e762..1430326a 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java @@ -40,12 +40,12 @@ public static class Verifiable { * Nonce value used in conjunction with all encrypted fields, * randomly generated for each request */ - public String Nonce=""; + public String Nonce; /** * Used to check that the correct key has been chosen */ - public String Verifier=""; + public String Verifier; } /** @@ -122,7 +122,7 @@ public Response(String request, String hash) { */ public String RequestType; - public String Error = ""; + public String Error; public boolean Success = false; @@ -130,12 +130,12 @@ public Response(String request, String hash) { * The user selected String as a result of 'associate', * always returned on every request */ - public String Id=""; + public String Id; /** * response to get-logins-count, number of entries for requested Url */ - public Integer Count; + public Integer Count=0; /** * response the current version of KeePassHttp diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java index 5b691abc..52306531 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java @@ -4,6 +4,7 @@ import org.linguafranca.pwdb.Entry; import org.linguafranca.pwdb.keepasshttp.Message.ResponseEntry; +import java.io.IOException; import java.text.SimpleDateFormat; import java.util.*; @@ -20,12 +21,16 @@ private interface MessageProcessor { private final Database database; private final PwGenerator pwGenerator; + private final DatabaseAdaptor adaptor; + private Map processors = new HashMap<>(); - Processor(Database database, PwGenerator pwGenerator) { - this.database = database; - this.pwGenerator = pwGenerator; + Processor(DatabaseAdaptor adaptor) { + this.database = adaptor.getDatabase(); + this.pwGenerator = adaptor.getPwGenerator(); + this.adaptor = adaptor; + processors.put(Message.Type.TEST_ASSOCIATE, new TestAssociate()); processors.put(Message.Type.ASSOCIATE, new Associate()); processors.put(Message.Type.GET_LOGINS, new GetLogins()); @@ -39,10 +44,6 @@ void process(Message.Request request, Message.Response response) { processors.get(request.RequestType).process(request, response); } - private String makeId() { - return database.getName() + " (" + database.getRootGroup().getUuid().toString() + ")"; - } - private class GetLogins implements MessageProcessor { public void process(final Message.Request r, Message.Response resp) { @@ -58,7 +59,6 @@ public boolean matches(Entry entry) { resp.Entries.add(new ResponseEntry(entry.getTitle(), entry.getUsername(), entry.getPassword(), entry.getUuid().toString())); } resp.Count = resp.Entries.size(); - resp.Id = makeId(); resp.Success = true; } } @@ -75,7 +75,6 @@ public void process(Message.Request r, Message.Response resp) { String p = pwGenerator.generate(); resp.Entries.add(new ResponseEntry("Password", "login", p, UUID.randomUUID().toString())); resp.Count = resp.Entries.size(); - resp.Id = makeId(); resp.Success = true; } } @@ -93,7 +92,6 @@ public boolean matches(Entry entry) { resp.Entries.add(new ResponseEntry(entry.getTitle(), entry.getUsername(), entry.getPassword(), entry.getUuid().toString())); } resp.Count = resp.Entries.size(); - resp.Id = makeId(); resp.Success = true; } } @@ -115,15 +113,21 @@ public boolean matches(Entry entry) { if (entry == null) { entry = database.newEntry(); entry.setTitle("New Entry " + format.format(new Date())); + entry.setNotes("Created automatically"); + } else { + entry.setNotes(entry.getNotes() + "\nUpdated " + format.format(new Date())); } entry.setPassword(r.Password); entry.setUsername(r.Login); entry.setUrl(r.Url); entry.setProperty("SubmitUrl", r.SubmitUrl); - entry.setNotes("Created automatically"); //noinspection unchecked database.getRootGroup().addEntry(entry); - resp.Id = makeId(); + try { + database.save(adaptor.getCredentials(), adaptor.getOutputStream()); + } catch (IOException e) { + throw new IllegalStateException(e); + } resp.Success = true; } } @@ -131,7 +135,6 @@ public boolean matches(Entry entry) { private class Associate implements MessageProcessor { @Override public void process(Message.Request request, Message.Response response) { - response.Id = makeId(); response.Success = true; } @@ -142,8 +145,7 @@ private class TestAssociate implements MessageProcessor { public void process(Message.Request request, Message.Response response) { response.Success = false; if (request.Id != null) { - response.Success = request.Id.equals(makeId()); - response.Id = makeId(); + response.Success = request.Id.equals(adaptor.getId()); } } diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/PwGenerator.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/PwGenerator.java index 9b061402..f6ad8b96 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/PwGenerator.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/PwGenerator.java @@ -1,8 +1,33 @@ package org.linguafranca.pwdb.keepasshttp; +import java.security.SecureRandom; +import java.util.Random; + /** * @author jo */ public interface PwGenerator { String generate(); + /** + * Generator for simple hex password + */ + class HexPwGenerator implements PwGenerator { + private final int length; + + HexPwGenerator (int length) { + this.length = length; + } + + @Override + public String generate() { + String[] symbols = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"}; + Random random = new SecureRandom(); + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + int indexRandom = random.nextInt( symbols.length ); + sb.append( symbols[indexRandom] ); + } + return sb.toString(); + } + } } diff --git a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java index e3a19702..42e008e0 100644 --- a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java +++ b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java @@ -4,8 +4,15 @@ import com.google.gson.GsonBuilder; import org.junit.Test; import org.linguafranca.pwdb.kdbx.Helpers; +import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import static org.junit.Assert.fail; + /** * @author jo */ @@ -21,15 +28,31 @@ public class MonitoredRequestsTest { Gson gson = new GsonBuilder().disableHtmlEscaping().create(); + File tempFile; + + public MonitoredRequestsTest() { + KdbxCreds creds = new KdbxCreds("123".getBytes()); + + SimpleDatabase db = new SimpleDatabase(); + db.setName("Test Database"); + try { + tempFile = File.createTempFile("pwdb", "tmp"); + db.save(creds, new FileOutputStream(tempFile)); + } catch (Exception e) { + fail(); + } + } @Test - public void sessionTest() { - Processor processor = new Processor(new SimpleDatabase(), new PwGenerator() { - @Override - public String generate() { - return "123"; - } - }); + public void sessionTest() throws Exception { + Processor processor = new Processor(new DatabaseAdaptor.Default(tempFile, + new KdbxCreds("123".getBytes()), + new PwGenerator() { + @Override + public String generate() { + return "123"; + } + })); Message.Request request = gson.fromJson(associateRequest, Message.Request.class); Crypto crypto = new Crypto(request.Key); @@ -47,7 +70,6 @@ public String generate() { crypto.verify(actualResponse); - request = gson.fromJson(testAssociateRequestWithId, Message.Request.class); actualResponse = gson.fromJson(testAssociateResponseWithId, Message.Response.class); @@ -75,7 +97,7 @@ public String generate() { System.out.println(gson.toJson(actualResponse)); crypto.verify(actualResponse); byte[] iv = Helpers.decodeBase64Content(actualResponse.Nonce.getBytes(), false); - for (Message.ResponseEntry entry: actualResponse.Entries) { + for (Message.ResponseEntry entry : actualResponse.Entries) { System.out.println(crypto.decryptFromBase64(entry.Login, iv)); System.out.println(crypto.decryptFromBase64(entry.Name, iv)); System.out.println(crypto.decryptFromBase64(entry.Password, iv)); diff --git a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java index 8ec70f9a..e8ac9ff2 100644 --- a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java +++ b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java @@ -4,12 +4,17 @@ import com.google.gson.GsonBuilder; import org.junit.Test; import org.linguafranca.pwdb.kdbx.Helpers; +import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; import org.linguafranca.pwdb.keepasshttp.Crypto; import org.linguafranca.pwdb.keepasshttp.Processor; import org.linguafranca.pwdb.keepasshttp.Message; import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; import java.security.SecureRandom; import java.util.ArrayList; @@ -30,6 +35,20 @@ public class TestRequestResponse { private static String reResponse = "{\"RequestType\":\"test-associate\",\"Error\":\"\",\"Success\":true,\"Id\":null,\"Count\":null,\"Version\":\"1.8.4.1\",\"Hash\":\"d51377aeb06c1707f56c0b323662ddf41c777b0c\",\"Entries\":null,\"Nonce\":\"QaYPVoWvF+k31MQGuyLEEA==\",\"Verifier\":\"kOReuG3a0l+do6/8xC58QjNVqgMrAADvt7agyczuPtk=\"}"; private Gson gson = new GsonBuilder().disableHtmlEscaping().create(); + File tempFile; + public TestRequestResponse() { + KdbxCreds creds = new KdbxCreds("123".getBytes()); + + SimpleDatabase db = new SimpleDatabase(); + db.setName("Test Database"); + try { + tempFile = File.createTempFile("pwdb", "tmp"); + db.save(creds, new FileOutputStream(tempFile)); + }catch (Exception e) { + fail(); + } + + } @Test public void testVerifyAssociate() { Message.Request r = gson.fromJson(associate, Message.Request.class); @@ -60,7 +79,7 @@ public void testVerifyAssociateRequestResponse() { } @Test - public void testGetLogins() { + public void testGetLogins() throws Exception { Message.Request r = gson.fromJson(getLoginsAssociate, Message.Request.class); Crypto crypto = new Crypto(r.Key); crypto.verify(r); @@ -79,7 +98,14 @@ public void testGetLogins() { System.out.println(unencodedSubmitUrl); assertEquals("https://www.facebook.com/login.php?login_attempt=1&lwv=110", unencodedSubmitUrl); - Message.Response response = new Message.Response(l.RequestType, new DatabaseAdaptor.Default(new SimpleDatabase()).getHash()); + Message.Response response = new Message.Response(l.RequestType, new DatabaseAdaptor.Default(tempFile, + new KdbxCreds("123".getBytes()), + new PwGenerator() { + @Override + public String generate() { + return "123"; + } + }).getHash()); response.Success=true; response.Count=1; response.Entries.add(new Message.ResponseEntry("a", "b", "c","uuid", new ArrayList())); From ac6d8df08666603bbae11f85bd8f173b42a7bdb4 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Wed, 25 Jan 2017 10:46:57 +0000 Subject: [PATCH 024/228] Correct test --- .../pwdb/keepasshttp/TestRequestResponse.java | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java index e8ac9ff2..2e2afb20 100644 --- a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java +++ b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java @@ -86,17 +86,8 @@ public void testGetLogins() throws Exception { Message.Request l = gson.fromJson(getLogins, Message.Request.class); assertTrue(crypto.verify(l)); - String encodedUrl = l.Url; - PaddedBufferedBlockCipher cipher = crypto.getCipher(Crypto.CMode.DECRYPT, Helpers.decodeBase64Content(l.Nonce.getBytes(), false)); - String unencodedUrl = Crypto.CryptoTransform(encodedUrl, true, false, cipher); - System.out.println(unencodedUrl); - assertEquals("https://www.facebook.com", unencodedUrl); - - cipher.reset(); - String encodedSubmitUrl = l.SubmitUrl; - String unencodedSubmitUrl = Crypto.CryptoTransform(encodedSubmitUrl,true,false, cipher); - System.out.println(unencodedSubmitUrl); - assertEquals("https://www.facebook.com/login.php?login_attempt=1&lwv=110", unencodedSubmitUrl); + assertEquals("https://www.facebook.com", l.Url); + assertEquals("https://www.facebook.com/login.php?login_attempt=1&lwv=110", l.SubmitUrl); Message.Response response = new Message.Response(l.RequestType, new DatabaseAdaptor.Default(tempFile, new KdbxCreds("123".getBytes()), From 3c4960b6a4da5bbf4e588f1c06d5a45ec9b457a1 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 27 Jan 2017 10:37:58 +0000 Subject: [PATCH 025/228] Fix error on save of Protected attribute in Siimple "true" -> "True" --- CHANGELOG.md | 3 ++- .../pwdb/kdbx/KdbxOutputTransformer.java | 13 ++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d62cee08..4b55d169 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,13 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.com) and [Semantic Versioning](http://semver.org/spec/v2.0.0.html) -## Next +##[2.1.1] 2017-01-27 ### Fixed - Simple implementation not reading DeletedObjects correctly per Nigel Rook - Simple implementation CustomIcons optional per @Kin-k +- Simple implementation saving protected fields with Protected="true" instead of "True" ### Added diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxOutputTransformer.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxOutputTransformer.java index 9013d26c..1f48eeb5 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxOutputTransformer.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxOutputTransformer.java @@ -23,6 +23,9 @@ import javax.xml.stream.events.Attribute; import javax.xml.stream.events.XMLEvent; +import java.util.ArrayList; +import java.util.List; + import static javax.xml.stream.XMLStreamConstants.CHARACTERS; import static javax.xml.stream.XMLStreamConstants.END_ELEMENT; import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; @@ -47,7 +50,15 @@ public XMLEvent transform(XMLEvent event) { case START_ELEMENT: { Attribute attribute = event.asStartElement().getAttributeByName(new QName("Protected")); if (attribute != null) { - encryptContent = Helpers.toBoolean(attribute.getValue()); + encryptContent = Helpers.toBoolean(attribute.getValue()); // this is a workaround for Simple XML not calling converter on attributes + List attributes = new ArrayList<>(); + if (attribute.getValue().toLowerCase().equals("true")) { + attributes.add(eventFactory.createAttribute("Protected", "True")); + } + event = eventFactory.createStartElement( + event.asStartElement().getName(), + attributes.iterator(), + null); } break; } From d39083f2f604c81d31a4bf54bbaaa7e3fa0bd702 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 27 Jan 2017 10:38:36 +0000 Subject: [PATCH 026/228] Tidy Simple implementation files --- .../pwdb/kdbx/simple/SimpleEntry.java | 2 +- .../pwdb/kdbx/simple/SimpleGroup.java | 2 +- .../pwdb/kdbx/simple/model/EntryClasses.java | 19 +++++----- .../pwdb/kdbx/simple/model/KeePassFile.java | 2 - .../pwdb/kdbx/simple/model/Times.java | 13 ++++--- .../kdbx/simple/SimpleSaveAndReloadTest.java | 37 +++++++++++++++++-- 6 files changed, 53 insertions(+), 22 deletions(-) diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java index 366a8845..a7157fef 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java @@ -40,7 +40,7 @@ * * @author jo */ -@SuppressWarnings({"WeakerAccess"}) +@SuppressWarnings({"WeakerAccess", "unused"}) @Root(name="Entry") public class SimpleEntry extends AbstractEntry { @Element(name = "UUID", type=UUID.class) diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java index 131de74e..86bf7bdc 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java @@ -35,7 +35,7 @@ * Implementation of {@link Group} using the Simple XML framework. * @author jo */ -@SuppressWarnings("WeakerAccess") +@SuppressWarnings({"WeakerAccess", "unused"}) @Root(name = "Group") public class SimpleGroup extends org.linguafranca.pwdb.base.AbstractGroup { @Element(name = "UUID", type = UUID.class) diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/EntryClasses.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/EntryClasses.java index cb4fd553..b597b3aa 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/EntryClasses.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/EntryClasses.java @@ -16,7 +16,6 @@ package org.linguafranca.pwdb.kdbx.simple.model; -import org.linguafranca.pwdb.base.AbstractEntry; import org.linguafranca.pwdb.kdbx.simple.SimpleEntry; import org.linguafranca.pwdb.kdbx.simple.converter.KeePassBooleanConverter; import org.simpleframework.xml.Attribute; @@ -30,6 +29,7 @@ /** * @author jo */ +@SuppressWarnings("unused") public abstract class EntryClasses { public static StringProperty getStringProperty(String name, List string) { for (StringProperty property : string) { @@ -74,7 +74,6 @@ public static class Association { protected String window; @Element(name="KeystrokeSequence") protected String keystrokeSequence; - @SuppressWarnings("unused") protected List windowAndKeystrokeSequence; } } @@ -91,10 +90,10 @@ public StringProperty(String key, Value value) { } @Element(name="Key") - protected String key; + String key; @Element(name="Value") - protected Value value; + Value value; @Override public String getName() { @@ -126,12 +125,14 @@ public Value(String text, Boolean _protected) { @Attribute(name = "ProtectInMemory", required = false) @Convert(KeePassBooleanConverter.class) + // NB converters don't work on attributes -see KdbxOutputTransformer protected Boolean protectInMemory; @Attribute(name = "Protected", required = false) @Convert(KeePassBooleanConverter.class) - protected Boolean _protected; + // NB converters don't work on attributes -see KdbxOutputTransformer + Boolean _protected; @Text - protected String text; + String text; public void setProtected(boolean aProtected) { this._protected = aProtected; @@ -143,10 +144,10 @@ public void setProtected(boolean aProtected) { public static class BinaryProperty implements org.simpleframework.xml.util.Entry { @Element(name="Key") - protected String key; + String key; @Element(name="Value") - protected Value value; + Value value; public String getKey() { return key; @@ -168,7 +169,7 @@ public void setValue(Value value) { @Root(name="Value") public static class Value { @Attribute(name="Ref") - protected String ref; + String ref; public void setRef(String ref) { this.ref = ref; diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java index c2a33c13..d81a7c5c 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java @@ -17,7 +17,6 @@ package org.linguafranca.pwdb.kdbx.simple.model; import org.linguafranca.pwdb.Entry; -import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; import org.linguafranca.pwdb.kdbx.simple.SimpleGroup; import org.linguafranca.pwdb.kdbx.simple.converter.Base64ByteArrayConverter; import org.linguafranca.pwdb.kdbx.simple.converter.KeePassBooleanConverter; @@ -25,7 +24,6 @@ import org.linguafranca.pwdb.kdbx.simple.converter.UuidConverter; import org.simpleframework.xml.*; import org.simpleframework.xml.convert.Convert; -import org.simpleframework.xml.util.Dictionary; import java.util.ArrayList; import java.util.Date; diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/Times.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/Times.java index 20a8aa59..388b345f 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/Times.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/Times.java @@ -27,26 +27,27 @@ /** * @author jo */ +@SuppressWarnings({"unused", "WeakerAccess"}) @Root public class Times { - @Element(name = "LastModificationTime", required = true, type = Date.class) + @Element(name = "LastModificationTime", type = Date.class) @Convert(TimeConverter.class) protected Date lastModificationTime; - @Element(name = "CreationTime", required = true, type = Date.class) + @Element(name = "CreationTime", type = Date.class) @Convert(TimeConverter.class) protected Date creationTime; - @Element(name = "LastAccessTime", required = true, type = Date.class) + @Element(name = "LastAccessTime", type = Date.class) @Convert(TimeConverter.class) protected Date lastAccessTime; - @Element(name = "ExpiryTime", required = true, type = Date.class) + @Element(name = "ExpiryTime", type = Date.class) @Convert(TimeConverter.class) protected Date expiryTime; - @Element(name = "Expires", required = true, type = Boolean.class) + @Element(name = "Expires", type = Boolean.class) @Convert(KeePassBooleanConverter.class) protected Boolean expires; @Element(name = "UsageCount") protected int usageCount; - @Element(name = "LocationChanged", required = true, type = Date.class) + @Element(name = "LocationChanged", type = Date.class) @Convert(TimeConverter.class) protected Date locationChanged; diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java index a035fc9a..6dbcb6af 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java @@ -16,15 +16,18 @@ package org.linguafranca.pwdb.kdbx.simple; +import org.junit.Test; import org.linguafranca.pwdb.checks.SaveAndReloadChecks; import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.kdbx.StreamFormat; import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxHeader; +import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxSerializer; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.*; + +import static org.junit.Assert.assertTrue; /** * @author jo @@ -59,6 +62,34 @@ public Database loadDatabase(Credentials credentials, InputStream inputStream) t } } + + // check that boolean comes out in upper case - Simple Converters don't work on attributes + // so this is done in the output transformer + @Test + public void uppercaseBooleanTest() throws IOException { + SimpleDatabase s = new SimpleDatabase(); + SimpleEntry e = s.newEntry(); + e.setPassword("12345"); + s.getRootGroup().addEntry(e); + File file = File.createTempFile("kdbx", "kdbx"); + s.save(new KdbxCreds("123".getBytes()), new FileOutputStream(file)); + InputStream inputStream = new FileInputStream(file); + Credentials credentials = new KdbxCreds("123".getBytes()); + InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); + BufferedReader br = new BufferedReader(new InputStreamReader(decryptedInputStream)); + boolean foundValue = false; + while (br.ready()) { + String string = br.readLine(); + if (string.trim().startsWith(" Date: Fri, 27 Jan 2017 10:43:50 +0000 Subject: [PATCH 027/228] [maven-release-plugin] prepare release KeePassJava2-2.1.1 --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 4 ++-- simple/pom.xml | 2 +- test/pom.xml | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index fb7b9719..5a5f0099 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.1.1 ../pom.xml 4.0.0 diff --git a/database/pom.xml b/database/pom.xml index bc035e29..5905ee66 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.1.1 ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index 74e18c0f..45300ac9 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.1.1 ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index fa6af0e6..440faee5 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.1.1 ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 7bfa2b27..d049cf5f 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.1.1 ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index 2ae3af4e..1ac66765 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.1.1 ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index 07762de2..f8527120 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.1.1 ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index 9f495a1d..2d4d9811 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.1.1-SNAPSHOT + 2.1.1 database test @@ -50,7 +50,7 @@ scm:git:git://github.com/jorabin/KeePassJava2.git scm:git:git@github.com:jorabin/KeePassJava2.git https://github.com/jorabin/KeePassJava2 - HEAD + KeePassJava2-2.1.1 diff --git a/simple/pom.xml b/simple/pom.xml index a7d3615a..df746a35 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.1.1 ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index 41bb809e..c8509cfc 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.1.1 4.0.0 From ee9d2d38d31c911e49e0674e4511df0e906652d5 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 27 Jan 2017 10:43:57 +0000 Subject: [PATCH 028/228] [maven-release-plugin] prepare for next development iteration --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 4 ++-- simple/pom.xml | 2 +- test/pom.xml | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index 5a5f0099..0a39c4da 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1 + 2.1.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/database/pom.xml b/database/pom.xml index 5905ee66..faf6d190 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1 + 2.1.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index 45300ac9..4edc3c14 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1 + 2.1.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index 440faee5..fcf0c90c 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1 + 2.1.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index d049cf5f..2ffcb159 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1 + 2.1.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index 1ac66765..c3583b94 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1 + 2.1.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index f8527120..6d3d1108 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1 + 2.1.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index 2d4d9811..7af2ec75 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.1.1 + 2.1.2-SNAPSHOT database test @@ -50,7 +50,7 @@ scm:git:git://github.com/jorabin/KeePassJava2.git scm:git:git@github.com:jorabin/KeePassJava2.git https://github.com/jorabin/KeePassJava2 - KeePassJava2-2.1.1 + HEAD diff --git a/simple/pom.xml b/simple/pom.xml index df746a35..883413bb 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1 + 2.1.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index c8509cfc..5a984647 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1 + 2.1.2-SNAPSHOT 4.0.0 From b14f70221dedc5a0e0a818f25c91cb0e93f64358 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 27 Jan 2017 10:58:47 +0000 Subject: [PATCH 029/228] Update Maven Coordinates --- readme.md | 4 ++-- .../org/linguafranca/pwdb/kdbx/KdbxOutputTransformer.java | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 65448f48..7ee1d260 100644 --- a/readme.md +++ b/readme.md @@ -34,9 +34,9 @@ The composite POM is org.linguafranca.pwdb KeePassJava2 - 2.1.0 + 2.1.1 - at Maven Central. Note that the artifactId has become Camel Case compared with release 2.0.1. + at Maven Central. Note that the artifactId has become Camel Case from release 2.1.x onwards. There are also separate POMs for the various modules. The module structure is illustrated below under [Build from Source](#bfs). diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxOutputTransformer.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxOutputTransformer.java index 1f48eeb5..9cdac8d9 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxOutputTransformer.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxOutputTransformer.java @@ -50,7 +50,8 @@ public XMLEvent transform(XMLEvent event) { case START_ELEMENT: { Attribute attribute = event.asStartElement().getAttributeByName(new QName("Protected")); if (attribute != null) { - encryptContent = Helpers.toBoolean(attribute.getValue()); // this is a workaround for Simple XML not calling converter on attributes + encryptContent = Helpers.toBoolean(attribute.getValue()); + // this is a workaround for Simple XML not calling converter on attributes List attributes = new ArrayList<>(); if (attribute.getValue().toLowerCase().equals("true")) { attributes.add(eventFactory.createAttribute("Protected", "True")); From 22ccd1cebcda2c76a69e8561b7e9d55f9ed2132a Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 27 Jan 2017 11:15:48 +0000 Subject: [PATCH 030/228] Typos --- all/pom.xml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index 0a39c4da..a9c39f92 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -25,7 +25,7 @@ 4.0.0 KeePassJava2 - KeepassJava2 :: All + KeePassJava2 :: All KDB Database support as well as KDBX 3.1: DOM, JAXB and Simple implementations. diff --git a/pom.xml b/pom.xml index 7af2ec75..cfc01e4b 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ pom - KeepassJava2 + KeePassJava2 A Java 7 API for databases compatible with the renowned KeePass password safe for Windows https://github.com/jorabin/KeePassJava2 From a9ac96fc9a97ed20181f176c0457342fe0e66d44 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 27 Jan 2017 11:19:40 +0000 Subject: [PATCH 031/228] Tidies documentation etc. --- HttpDatabase.kdbx | Bin 0 -> 1582 bytes .../pwdb/keepasshttp/DatabaseAdaptor.java | 47 +++++++++++++++--- ...ver.java => ExampleKeePassHttpServer.java} | 8 ++- .../pwdb/keepasshttp/KeePassHttpHandler.java | 17 ++++--- .../pwdb/keepasshttp/Message.java | 1 - .../pwdb/keepasshttp/Processor.java | 8 +-- .../pwdb/keepasshttp/PwGenerator.java | 2 +- .../org/linguafranca/pwdb/kdbx/Helpers.java | 2 +- 8 files changed, 60 insertions(+), 25 deletions(-) create mode 100644 HttpDatabase.kdbx rename http/src/main/java/org/linguafranca/pwdb/keepasshttp/{KeePassHttpServer.java => ExampleKeePassHttpServer.java} (89%) diff --git a/HttpDatabase.kdbx b/HttpDatabase.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..243bb55dcf10f7c0436270738adfc1d5babb123b GIT binary patch literal 1582 zcmV+}2GRKg*`k_f`%AR}00RI55CAd3^5(yBLr}h01tDtuTK@wC0096100bZaBs_hS zZ%^VurSLN6i;(sX)Mt`p(GffoDQ_hq0irNl1t0(()IFjYFtz!9gQrKdCe6sB=lr%R z%~_QVbQ+g!ZiOrc2mo*w00000000LN0B9k_OK<=raF7WrdLnR2_OKU?m(@^>6XFEt{@RliH~}pH+eSy!dn7% zb#l9JJ?3Q!1ONg60000401XNa3JLqq%Mg|{4@S)fVHOf4OEKB$hc8FTaJPfU43~6W zi>Mv)sz{`5fMw>mGWF|fli9CgzUMl7!t(5p=ND*s(Q zXUw|Xj%=ZZIu!1|;0yQ?TAPdPKQpUb)$~A*;=F(7E@o%KPGT+lxMQkF+p;xtA?2_NrcS`+?X7Qs94u3dvl)r%7%QoSsJ(6bofgLqsg zs=#Zs2YXm<;-t-dy41E};FrO~;z8R;4CDupeI3G@G2NeE-e54|w9TN4nf}da+u4H0TKl zCtaKcA>2^=CO*CE@feE93Ld^TmxAVSNspWEQgU4PYw(i)>NpjC+L+%!C5@&*DI3vm z64;r$Qm!Jr4GrJVX$iOmx4QE9^V_kC_4|V`9CE)_~+O=LwD5znjihOHi1diXdcNs96=VT8Sd{~)8 zV`|?}8B;1fIc8ZSN2YxKW3}C|ji{cxAs1z}<=Gal5zPN41*FYv(sgx;G5iMJE8YIX z;v$*X`;lc_4(iq|V7LVYGg^Vwin<@4)g@}Z_uaR<9(!dPhIY_372jSP*)aU_itz)@d+%R=;r541>}F{dzN! zy(9MU2Yx*a`m^gv+Vpx+_|7Xs-+1l+u`hip+yEB=<8>JfYL_WYj`o~#c7eedBjF*C zr5;0H_E4g7V);mu4Fd>&X(4wKyA$VXp)Wu$F5);#!kb)WI#_V8d_f>H`;Y2xep&}6 z*6P5oA1Uyn^#oy-AWu0!U9;27^${9XaBT#H*vb&HD7Kzro9cn9>0z10s$n`xqZn%P`nQ85x&hXAc^(lK zMmZ5+E(C61Ye5G1?>`$a0KH--lPqeBQd*v9eieqFOdD1z$LnQ|&AagX&hz<&>O*1< z^i~UQ%9i3o4FD6}C!)4ywi*S;giWPrK?6Io+C2{$AT9eK{J-1WeUoH-x0VA+Y&KI^ g2Pc*rwT4jD`Wp Entries; -// public String objectName = ""; } public static class ResponseEntry { diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java index 52306531..603501bb 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java @@ -10,8 +10,6 @@ /** * Contains message processors for processing messages (doh) - * - * @author jo */ class Processor { @@ -41,7 +39,11 @@ private interface MessageProcessor { } void process(Message.Request request, Message.Response response) { - processors.get(request.RequestType).process(request, response); + MessageProcessor mp = processors.get(request.RequestType); + if (mp == null) { + throw new IllegalStateException("Unknown message type " + request.RequestType); + } + mp.process(request, response); } private class GetLogins implements MessageProcessor { diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/PwGenerator.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/PwGenerator.java index f6ad8b96..3f00d39a 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/PwGenerator.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/PwGenerator.java @@ -4,7 +4,7 @@ import java.util.Random; /** - * @author jo + * Interface for an injectable password generator */ public interface PwGenerator { String generate(); diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java index 9afefd58..d8225c2a 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java @@ -71,7 +71,7 @@ public static Boolean toBoolean(String value) { } public static String fromBoolean(Boolean value) { - return value == null ? "False" : value ? "True" : "False"; + return value == null ? "False" : (value ? "True" : "False"); } private static SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); From bdbc0e4f429835c66e926b33464f1024e5773e62 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 27 Jan 2017 11:29:09 +0000 Subject: [PATCH 032/228] Update HTTP POM to 2.1.2-SNAPSHOT --- http/pom.xml | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/http/pom.xml b/http/pom.xml index 0b99150e..af1f9ed1 100644 --- a/http/pom.xml +++ b/http/pom.xml @@ -5,22 +5,17 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.1.2-SNAPSHOT 4.0.0 KeePassJava2-http KeePassJava2 :: HTTP - - com.madgag.spongycastle - core - 1.54.0.0 - org.linguafranca.pwdb - KeePassJava2-kdbx - 2.1.1-SNAPSHOT + KeePassJava2-simple + 2.1.2-SNAPSHOT org.eclipse.jetty @@ -53,12 +48,6 @@ 4.12 test - - org.linguafranca.pwdb - KeePassJava2-simple - 2.1.1-SNAPSHOT - - \ No newline at end of file From 6cbd70cb68af459bad1f8d5c36f09c7f674bdbb8 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 27 Jan 2017 12:00:01 +0000 Subject: [PATCH 033/228] updating odcumentation --- CHANGELOG.md | 7 +++++++ http/pom.xml | 4 ++-- http/readme.md | 22 ++++++++++++++++++++++ readme.md | 3 +++ 4 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 http/readme.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b55d169..58abd329 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.com) and [Semantic Versioning](http://semver.org/spec/v2.0.0.html) +##[2.1.2-SNAPSHOT] + +### Added + +An experimental implementation of [keepasshttp](https://github.com/pfn/keepasshttp/) see +[thereadme](http/readme.md) for warnings, limitations, etc. about this. + ##[2.1.1] 2017-01-27 ### Fixed diff --git a/http/pom.xml b/http/pom.xml index af1f9ed1..216a3915 100644 --- a/http/pom.xml +++ b/http/pom.xml @@ -9,8 +9,8 @@ 4.0.0 - KeePassJava2-http - KeePassJava2 :: HTTP + keepasshttp + PWDB :: keepasshttp org.linguafranca.pwdb diff --git a/http/readme.md b/http/readme.md new file mode 100644 index 00000000..dc4f84ec --- /dev/null +++ b/http/readme.md @@ -0,0 +1,22 @@ +#Keepasshttp module + +**_Experimental and Unsafe_** + +This is a Java implementation of the [keepasshttp plugin](https://github.com/pfn/keepasshttp/) +for Windows KeePass. + +It is intended to provide support for the [passifox and chromeipass](https://github.com/pfn/passifox) +browser extensions that allow pasting of credentials from a KeePassDatabse +in a browser context. + +This implementation is an initial one and interworking may not be as good as it could be. + +Also you should note that it does not use https so it is not secure. + +Finally, you should note that any client can connect to the server since the protocol +does not support client credentials. So if you expose this server to the world, +then anyone with a compatible browser plugin can in theory connect +to your database and extract all your secrets. + +**You have been warned sufficently that no representation is made as to +fitness for purpose, safety or anything else.** \ No newline at end of file diff --git a/readme.md b/readme.md index 7ee1d260..a65f808a 100644 --- a/readme.md +++ b/readme.md @@ -170,6 +170,9 @@ Each module corresponds to a Maven artifact. The GroupId is `org.linguafranca.pw bindings might be useful for building other interfaces. domKeePassJava2-domA DOM based implementation of KDBX. Being DOM based it is rather slow, but messes less with existing content than the other two implementations. Known to work on Android. +httpkeepasshttpAn implementation of keepasshttp, which is a plugin for Windows Keepass +supporting communication with Chrome (chromeIPass) and Firefox(PassIFox) extensions. It is experimental and usafe. Please +read and inwardly digest the readme. From 698f2b203a455bd594b80e548a9e0e6918d8f4f0 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 27 Jan 2017 15:36:57 +0000 Subject: [PATCH 034/228] typo --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index a65f808a..a2b5a705 100644 --- a/readme.md +++ b/readme.md @@ -172,7 +172,7 @@ bindings might be useful for building other interfaces. messes less with existing content than the other two implementations. Known to work on Android. httpkeepasshttpAn implementation of keepasshttp, which is a plugin for Windows Keepass supporting communication with Chrome (chromeIPass) and Firefox(PassIFox) extensions. It is experimental and usafe. Please -read and inwardly digest the readme. +read and inwardly digest the readme. From 94ac52e16d3d503727faf7ca0e9718e1c24f4fe8 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 27 Jan 2017 18:55:26 +0000 Subject: [PATCH 035/228] Adding in recycle bin functionality --- .../java/org/linguafranca/pwdb/Database.java | 56 ++++++++++++ .../java/org/linguafranca/pwdb/Group.java | 5 + .../pwdb/base/AbstractDatabase.java | 91 +++++++++++++++++++ .../linguafranca/pwdb/base/AbstractGroup.java | 6 ++ .../pwdb/kdbx/dom/DomDatabaseWrapper.java | 46 +++++++++- .../pwdb/kdbx/dom/DomGroupWrapper.java | 9 ++ .../linguafranca/pwdb/kdbx/dom/DomHelper.java | 4 + .../pwdb/kdbx/dom/DomRecycleBinTest.java | 14 +++ .../pwdb/kdbx/jaxb/JaxbDatabase.java | 26 ++++++ .../pwdb/kdbx/jaxb/JaxbGroup.java | 5 + .../pwdb/kdbx/jaxb/JaxbRecycleBinTest.java | 12 +++ .../linguafranca/pwdb/kdb/KdbDatabase.java | 17 ++++ .../org/linguafranca/pwdb/kdb/KdbGroup.java | 5 + .../pwdb/kdbx/simple/SimpleDatabase.java | 25 ++++- .../pwdb/kdbx/simple/SimpleGroup.java | 5 + .../pwdb/kdbx/simple/model/KeePassFile.java | 6 +- .../kdbx/simple/SimpleRecycleBinTest.java | 12 +++ .../pwdb/checks/RecycleBinChecks.java | 79 ++++++++++++++++ 18 files changed, 418 insertions(+), 5 deletions(-) create mode 100644 dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomRecycleBinTest.java create mode 100644 jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbRecycleBinTest.java create mode 100644 simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleRecycleBinTest.java create mode 100644 test/src/main/java/org/linguafranca/pwdb/checks/RecycleBinChecks.java diff --git a/database/src/main/java/org/linguafranca/pwdb/Database.java b/database/src/main/java/org/linguafranca/pwdb/Database.java index bed7ca7a..7f0ed064 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Database.java +++ b/database/src/main/java/org/linguafranca/pwdb/Database.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.io.OutputStream; import java.util.List; +import java.util.UUID; /** * Interface for a password database consisting of Groups, sub-Groups and Entries. @@ -112,6 +113,61 @@ public interface Database , G extends Group, G extends Group entry) { // everything else should have been copied via properties return result; } + + @Override + public E findEntry(final UUID uuid) { + List entries = findEntries(new Entry.Matcher() { + @Override + public boolean matches(Entry entry) { + return entry.getUuid().equals(uuid); + } + }); + if (entries.size() > 1) { + throw new IllegalStateException("Two entries same UUID"); + } + if (entries.size() == 0) { + return null; + } + return entries.get(0); + } + + @Override + public boolean deleteEntry(final UUID uuid) { + E e = findEntry(uuid); + if (e == null) { + return false; + } + e.getParent().removeEntry(e); + if (isRecycleBinEnabled()) { + getRecycleBin().addEntry(e); + } + return true; + } + + @Override + public G findGroup(final UUID uuid){ + final List groups = new ArrayList<>(); + visit(new Visitor.Default() { + // ignore sub groups of the recycle bin + boolean recycle; + @Override + public void startVisit(Group group) { + if (!recycle && group.getUuid().equals(uuid)) { + groups.add((G) group); + } + if (group.isRecycleBin()) { + recycle = true; + } + } + + @Override + public void endVisit(Group group) { + if (group.isRecycleBin()) { + recycle = false; + } + } + }); + if (groups.size() > 1) { + throw new IllegalStateException("Two groups same UUID"); + } + if (groups.size() == 0) { + return null; + } + return groups.get(0); + } + + @Override + public boolean deleteGroup(final UUID uuid) { + G g = findGroup(uuid); + if (g==null) { + return false; + } + g.getParent().removeGroup(g); + if (isRecycleBinEnabled()) { + getRecycleBin().addGroup(g); + } + return true; + } + + @Override + public void emptyRecycleBin() { + G recycle = getRecycleBin(); + if (recycle == null) { + return; + } + for (G g: recycle.getGroups()){ + recycle.removeGroup(g); + } + for (E e: recycle.getEntries()){ + recycle.removeEntry(e); + } + } } diff --git a/database/src/main/java/org/linguafranca/pwdb/base/AbstractGroup.java b/database/src/main/java/org/linguafranca/pwdb/base/AbstractGroup.java index eba0a505..727ea7bc 100644 --- a/database/src/main/java/org/linguafranca/pwdb/base/AbstractGroup.java +++ b/database/src/main/java/org/linguafranca/pwdb/base/AbstractGroup.java @@ -46,6 +46,9 @@ public List findGroups(String group1) { @Override public List findEntries(String find, boolean recursive) { List result = new ArrayList<>(getEntries().size()); + if (isRecycleBin()) { + return result; + } for (E entry: getEntries()){ if (entry.match(find)){ result.add(entry); @@ -62,6 +65,9 @@ public List findEntries(String find, boolean recursive) { @Override public List findEntries(Entry.Matcher matcher, boolean recursive) { List result = new ArrayList<>(getEntries().size()); + if (isRecycleBin()) { + return result; + } for (E entry: getEntries()){ if (entry.match(matcher)){ result.add(entry); diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java index 14383884..f3389a7a 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java @@ -16,7 +16,9 @@ package org.linguafranca.pwdb.kdbx.dom; +import org.linguafranca.pwdb.Entry; import org.linguafranca.pwdb.base.AbstractDatabase; +import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxStreamFormat; import org.linguafranca.pwdb.kdbx.StreamFormat; import org.linguafranca.pwdb.Credentials; @@ -28,6 +30,10 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.UUID; + +import static org.linguafranca.pwdb.kdbx.Helpers.base64FromUuid; +import static org.linguafranca.pwdb.kdbx.dom.DomHelper.*; /** * The class wraps a {@link DomSerializableDatabase} as a {@link org.linguafranca.pwdb.Database}. @@ -38,7 +44,7 @@ public class DomDatabaseWrapper extends AbstractDatabase{ - private KeePassFile keePassFile; + KeePassFile keePassFile; /** * Create a new empty database @@ -99,6 +99,29 @@ public SimpleIcon newIcon(Integer integer) { return ic; } + @Override + public boolean isRecycleBinEnabled() { + return this.keePassFile.meta.recycleBinEnabled; + } + + @Override + public void enableRecycleBin(boolean enable) { + this.keePassFile.meta.recycleBinEnabled = enable; + } + + @Override + public SimpleGroup getRecycleBin() { + UUID recycleBinUuid = this.keePassFile.meta.recycleBinUUID; + SimpleGroup g = findGroup(recycleBinUuid); + if (g == null && isRecycleBinEnabled()) { + g = newGroup("Recycle Bin"); + getRootGroup().addGroup(g); + this.keePassFile.meta.recycleBinUUID = g.getUuid(); + this.keePassFile.meta.recycleBinChanged = new Date(); + } + return g; + } + @Override public String getName() { return keePassFile.meta.databaseName; diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java index 86bf7bdc..703b3e26 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java @@ -93,6 +93,11 @@ public boolean isRootGroup() { return database.getRootGroup().equals(this); } + @Override + public boolean isRecycleBin() { + return database.keePassFile.meta.recycleBinUUID.equals(this.uuid); + } + @Override public SimpleGroup getParent() { return parent; diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java index d81a7c5c..9668d5e9 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java @@ -96,13 +96,13 @@ public static class Meta { protected ArrayList customIcons; @Element(name = "RecycleBinEnabled", type = Boolean.class) @Convert(KeePassBooleanConverter.class) - protected Boolean recycleBinEnabled; + public Boolean recycleBinEnabled; @Element(name = "RecycleBinUUID", type = UUID.class) @Convert(UuidConverter.class) - protected UUID recycleBinUUID; + public UUID recycleBinUUID; @Element(name = "RecycleBinChanged", type = Date.class) @Convert(TimeConverter.class) - protected Date recycleBinChanged; + public Date recycleBinChanged; @Element(name = "EntryTemplatesGroup", type = UUID.class) @Convert(UuidConverter.class) protected UUID entryTemplatesGroup; diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleRecycleBinTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleRecycleBinTest.java new file mode 100644 index 00000000..3d7a4895 --- /dev/null +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleRecycleBinTest.java @@ -0,0 +1,12 @@ +package org.linguafranca.pwdb.kdbx.simple; + +import org.linguafranca.pwdb.checks.RecycleBinChecks; + +/** + * @author jo + */ +public class SimpleRecycleBinTest extends RecycleBinChecks { + public SimpleRecycleBinTest() { + database = new SimpleDatabase(); + } +} diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/RecycleBinChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/RecycleBinChecks.java new file mode 100644 index 00000000..3fb007ae --- /dev/null +++ b/test/src/main/java/org/linguafranca/pwdb/checks/RecycleBinChecks.java @@ -0,0 +1,79 @@ +package org.linguafranca.pwdb.checks; + +import org.junit.Test; +import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.Entry; +import org.linguafranca.pwdb.Group; +import org.linguafranca.pwdb.Icon; + +import static org.junit.Assert.*; + +/** + * @author jo + */ +public abstract class RecycleBinChecks , G extends Group, E extends Entry, I extends Icon> { + + protected Database database; + + @Test + public void recycleBinEntries() { + database.enableRecycleBin(false); + assertFalse(database.isRecycleBinEnabled()); + assertNull(database.getRecycleBin()); + database.enableRecycleBin(true); + assertTrue(database.isRecycleBinEnabled()); + G recycleBin = database.getRecycleBin(); + assertNotNull(recycleBin); + + E entry = database.newEntry(); + assertEquals(entry, database.getRootGroup().addEntry(entry)); + assertEquals(1, database.getRootGroup().getEntriesCount()); + + assertTrue(database.deleteEntry(entry.getUuid())); + assertEquals(0, database.getRootGroup().getEntriesCount()); + assertEquals(1, recycleBin.getEntriesCount()); + + database.enableRecycleBin(false); + assertFalse(database.isRecycleBinEnabled()); + + entry = database.newEntry(); + assertEquals(entry, database.getRootGroup().addEntry(entry)); + assertEquals(1, database.getRootGroup().getEntriesCount()); + + assertTrue(database.deleteEntry(entry.getUuid())); + assertEquals(0, database.getRootGroup().getEntriesCount()); + assertEquals(1, recycleBin.getEntriesCount()); + database.emptyRecycleBin(); + assertEquals(0, recycleBin.getEntriesCount()); + } + + @Test + public void recycleBinGroups() { + database.enableRecycleBin(true); + assertTrue(database.isRecycleBinEnabled()); + G recycleBin = database.getRecycleBin(); + assertNotNull(recycleBin); + + G group= database.newGroup(); + assertEquals(group, database.getRootGroup().addGroup(group)); + assertEquals(2, database.getRootGroup().getGroupsCount()); + + assertTrue(database.deleteGroup(group.getUuid())); + assertEquals(1, database.getRootGroup().getGroupsCount()); + assertEquals(1, recycleBin.getGroupsCount()); + + database.enableRecycleBin(false); + assertFalse(database.isRecycleBinEnabled()); + + group = database.newGroup(); + assertEquals(group, database.getRootGroup().addGroup(group)); + assertEquals(2, database.getRootGroup().getGroupsCount()); + + assertTrue(database.deleteGroup(group.getUuid())); + assertEquals(1, database.getRootGroup().getGroupsCount()); + assertEquals(1, recycleBin.getGroupsCount()); + database.emptyRecycleBin(); + assertEquals(0, recycleBin.getGroupsCount()); + } + +} From a59322ea4b6817df3c31aec1add5fcea06d4282c Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 20 Jan 2018 14:22:26 +0000 Subject: [PATCH 036/228] Correct Typos --- all/pom.xml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index 0a39c4da..a9c39f92 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -25,7 +25,7 @@ 4.0.0 KeePassJava2 - KeepassJava2 :: All + KeePassJava2 :: All KDB Database support as well as KDBX 3.1: DOM, JAXB and Simple implementations. diff --git a/pom.xml b/pom.xml index 7af2ec75..cfc01e4b 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ pom - KeepassJava2 + KeePassJava2 A Java 7 API for databases compatible with the renowned KeePass password safe for Windows https://github.com/jorabin/KeePassJava2 From 69e9e73812723bd57aa1972a3353330c87f51d1c Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 20 Jan 2018 15:08:15 +0000 Subject: [PATCH 037/228] Fix for split package `kdbx` caused by simple implementation --- CHANGELOG.md | 6 ++++++ readme.md | 2 +- .../org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java | 4 ++-- .../kdbx/{ => simple/transformer}/KdbxInputTransformer.java | 6 +++++- .../{ => simple/transformer}/KdbxOutputTransformer.java | 6 +++++- .../linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java | 1 + .../linguafranca/pwdb/kdbx/KdbxInnerOutputStreamTest.java | 5 +---- 7 files changed, 21 insertions(+), 9 deletions(-) rename simple/src/main/java/org/linguafranca/pwdb/kdbx/{ => simple/transformer}/KdbxInputTransformer.java (92%) rename simple/src/main/java/org/linguafranca/pwdb/kdbx/{ => simple/transformer}/KdbxOutputTransformer.java (93%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b55d169..b830d333 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.com) and [Semantic Versioning](http://semver.org/spec/v2.0.0.html) +##[2.1.2] 2018-01-20 + +### Fixed + +- [Issue #16] Fix for split package + ##[2.1.1] 2017-01-27 ### Fixed diff --git a/readme.md b/readme.md index 7ee1d260..6d916b59 100644 --- a/readme.md +++ b/readme.md @@ -34,7 +34,7 @@ The composite POM is org.linguafranca.pwdb KeePassJava2 - 2.1.1 + 2.1.2 at Maven Central. Note that the artifactId has become Camel Case from release 2.1.x onwards. diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java index 3f879655..a7c0cc11 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java @@ -17,8 +17,8 @@ package org.linguafranca.pwdb.kdbx.simple; import org.linguafranca.pwdb.base.AbstractDatabase; -import org.linguafranca.pwdb.kdbx.KdbxInputTransformer; -import org.linguafranca.pwdb.kdbx.KdbxOutputTransformer; +import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxInputTransformer; +import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxOutputTransformer; import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxHeader; import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxSerializer; import org.linguafranca.pwdb.kdbx.stream_3_1.Salsa20StreamEncryptor; diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxInputTransformer.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxInputTransformer.java similarity index 92% rename from simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxInputTransformer.java rename to simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxInputTransformer.java index 5f208a14..6175a0f1 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxInputTransformer.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxInputTransformer.java @@ -14,8 +14,10 @@ * limitations under the License. */ -package org.linguafranca.pwdb.kdbx; +package org.linguafranca.pwdb.kdbx.simple.transformer; +import org.linguafranca.pwdb.kdbx.Helpers; +import org.linguafranca.pwdb.kdbx.StreamEncryptor; import org.linguafranca.xml.XmlEventTransformer; import javax.xml.namespace.QName; @@ -28,6 +30,8 @@ import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; /** + * Transform protected elements on input + * * @author jo */ @SuppressWarnings("WeakerAccess") diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxOutputTransformer.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxOutputTransformer.java similarity index 93% rename from simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxOutputTransformer.java rename to simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxOutputTransformer.java index 9cdac8d9..52690ec4 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxOutputTransformer.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxOutputTransformer.java @@ -14,8 +14,10 @@ * limitations under the License. */ -package org.linguafranca.pwdb.kdbx; +package org.linguafranca.pwdb.kdbx.simple.transformer; +import org.linguafranca.pwdb.kdbx.Helpers; +import org.linguafranca.pwdb.kdbx.StreamEncryptor; import org.linguafranca.xml.XmlEventTransformer; import javax.xml.namespace.QName; @@ -31,6 +33,8 @@ import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; /** + * Transform protected elements on output + * * @author jo */ @SuppressWarnings("WeakerAccess") diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java index 307c3ce2..b674508d 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java @@ -17,6 +17,7 @@ package org.linguafranca.pwdb.kdbx; import org.junit.Test; +import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxInputTransformer; import org.linguafranca.xml.XmlInputStreamFilter; import javax.xml.stream.XMLStreamException; diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerOutputStreamTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerOutputStreamTest.java index e16def67..8f6a3856 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerOutputStreamTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerOutputStreamTest.java @@ -19,18 +19,15 @@ import com.google.common.io.ByteStreams; import org.junit.Ignore; import org.junit.Test; -import org.linguafranca.pwdb.Database; -import org.linguafranca.pwdb.Entry; import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; import org.linguafranca.pwdb.kdbx.simple.SimpleEntry; -import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxOutputTransformer; import org.linguafranca.xml.XmlEventTransformer; import org.linguafranca.xml.XmlInputStreamFilter; import org.linguafranca.xml.XmlOutputStreamFilter; import javax.xml.stream.XMLStreamException; import java.io.*; -import java.security.SecureRandom; import static org.junit.Assert.assertEquals; From dc0438dec17674642e3f3032c14904898531ae02 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 20 Jan 2018 15:12:48 +0000 Subject: [PATCH 038/228] [maven-release-plugin] prepare release KeePassJava2-2.1.2 --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 4 ++-- simple/pom.xml | 2 +- test/pom.xml | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index a9c39f92..97e92f2a 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2-SNAPSHOT + 2.1.2 ../pom.xml 4.0.0 diff --git a/database/pom.xml b/database/pom.xml index faf6d190..3c07bc8a 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2-SNAPSHOT + 2.1.2 ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index 4edc3c14..e75cf1c8 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2-SNAPSHOT + 2.1.2 ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index fcf0c90c..233a81a6 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2-SNAPSHOT + 2.1.2 ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 2ffcb159..aadbeeae 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2-SNAPSHOT + 2.1.2 ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index c3583b94..99f91ed4 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2-SNAPSHOT + 2.1.2 ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index 6d3d1108..02144b47 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2-SNAPSHOT + 2.1.2 ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index cfc01e4b..d1213bc7 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.1.2-SNAPSHOT + 2.1.2 database test @@ -50,7 +50,7 @@ scm:git:git://github.com/jorabin/KeePassJava2.git scm:git:git@github.com:jorabin/KeePassJava2.git https://github.com/jorabin/KeePassJava2 - HEAD + KeePassJava2-2.1.2 diff --git a/simple/pom.xml b/simple/pom.xml index 883413bb..23a123ad 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2-SNAPSHOT + 2.1.2 ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index 5a984647..b857558c 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2-SNAPSHOT + 2.1.2 4.0.0 From 1ed15fa518730c8456752cdb551dce76bc0ad9c0 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 20 Jan 2018 15:12:54 +0000 Subject: [PATCH 039/228] [maven-release-plugin] prepare for next development iteration --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 4 ++-- simple/pom.xml | 2 +- test/pom.xml | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index 97e92f2a..54839683 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2 + 2.1.3-SNAPSHOT ../pom.xml 4.0.0 diff --git a/database/pom.xml b/database/pom.xml index 3c07bc8a..2056e55e 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2 + 2.1.3-SNAPSHOT ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index e75cf1c8..7cf15980 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2 + 2.1.3-SNAPSHOT ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index 233a81a6..75b4875d 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2 + 2.1.3-SNAPSHOT ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index aadbeeae..42048428 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2 + 2.1.3-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index 99f91ed4..1f8d7e8a 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2 + 2.1.3-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index 02144b47..d04bb908 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2 + 2.1.3-SNAPSHOT ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index d1213bc7..3f399b18 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.1.2 + 2.1.3-SNAPSHOT database test @@ -50,7 +50,7 @@ scm:git:git://github.com/jorabin/KeePassJava2.git scm:git:git@github.com:jorabin/KeePassJava2.git https://github.com/jorabin/KeePassJava2 - KeePassJava2-2.1.2 + HEAD diff --git a/simple/pom.xml b/simple/pom.xml index 23a123ad..d53a5241 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2 + 2.1.3-SNAPSHOT ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index b857558c..5db06d7d 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2 + 2.1.3-SNAPSHOT 4.0.0 From fa82bb4f0bbe4374ea41c37c0e2a1f78ab56f3cc Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 20 Jan 2018 15:28:43 +0000 Subject: [PATCH 040/228] Switch travis to openjdk - see https://github.com/travis-ci/travis-ci/issues/7884 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b0513495..3d066ab9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,3 @@ language: java -jdk: oraclejdk7 +jdk: openjdk7 script: mvn clean test From d94d7f89648cf34e4e92204ecc0b2d99e4dfde1b Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 20 Jan 2018 16:19:08 +0000 Subject: [PATCH 041/228] ignore failing test for now --- .../org/linguafranca/pwdb/example/SimpleQuickStartTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example/src/test/java/org/linguafranca/pwdb/example/SimpleQuickStartTest.java b/example/src/test/java/org/linguafranca/pwdb/example/SimpleQuickStartTest.java index 1e2707ee..7c11fd60 100644 --- a/example/src/test/java/org/linguafranca/pwdb/example/SimpleQuickStartTest.java +++ b/example/src/test/java/org/linguafranca/pwdb/example/SimpleQuickStartTest.java @@ -16,6 +16,7 @@ package org.linguafranca.pwdb.example; +import org.junit.Ignore; import org.junit.Test; import org.linguafranca.pwdb.kdbx.QuickStart; import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; @@ -59,7 +60,7 @@ public void loadTest() throws IOException { super.loadKdbx(); } - @Test + @Test @Ignore public void loadKdbSaveVernacular() throws IOException { super.loadKdb(); } From 89e851b3bd7e68f8655338399df68f1520abc70b Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 20 Jan 2018 16:38:58 +0000 Subject: [PATCH 042/228] failing intermittent travis fails with openjdk7 try oraclejdk8 --- .travis.yml | 2 +- .../org/linguafranca/pwdb/example/SimpleQuickStartTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3d066ab9..7fdf6ecf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,3 @@ language: java -jdk: openjdk7 +jdk: oraclejdk8 script: mvn clean test diff --git a/example/src/test/java/org/linguafranca/pwdb/example/SimpleQuickStartTest.java b/example/src/test/java/org/linguafranca/pwdb/example/SimpleQuickStartTest.java index 7c11fd60..07b08797 100644 --- a/example/src/test/java/org/linguafranca/pwdb/example/SimpleQuickStartTest.java +++ b/example/src/test/java/org/linguafranca/pwdb/example/SimpleQuickStartTest.java @@ -60,7 +60,7 @@ public void loadTest() throws IOException { super.loadKdbx(); } - @Test @Ignore + @Test public void loadKdbSaveVernacular() throws IOException { super.loadKdb(); } From 4ef90e2dc0b5d7d5671e4835f6c0fc7a27ee5944 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sun, 21 Jan 2018 15:08:07 +0000 Subject: [PATCH 043/228] Change instantiation order of piped streams --- .../xml/XmlOutputStreamFilter.java | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/simple/src/main/java/org/linguafranca/xml/XmlOutputStreamFilter.java b/simple/src/main/java/org/linguafranca/xml/XmlOutputStreamFilter.java index beb3eff8..20431365 100644 --- a/simple/src/main/java/org/linguafranca/xml/XmlOutputStreamFilter.java +++ b/simple/src/main/java/org/linguafranca/xml/XmlOutputStreamFilter.java @@ -42,36 +42,40 @@ @SuppressWarnings({"WeakerAccess", "unused"}) public class XmlOutputStreamFilter extends PipedOutputStream { - Future future; + protected Future future; + protected PipedInputStream pipedInputStream; public XmlOutputStreamFilter(final OutputStream outputStream, final XmlEventTransformer eventTransformer) throws IOException { + super(); + pipedInputStream = new PipedInputStream(this); + Callable output = new Callable() { public Boolean call() { - try { - XMLEventReader eventReader = new com.fasterxml.aalto.stax.InputFactoryImpl() - .createXMLEventReader(new PipedInputStream(XmlOutputStreamFilter.this)); - XMLEventWriter eventWriter = new com.fasterxml.aalto.stax.OutputFactoryImpl() - .createXMLEventWriter(outputStream); - - XMLEvent event = null; - while (eventReader.hasNext()) { - event = eventReader.nextEvent(); - event = eventTransformer.transform(event); - eventWriter.add(event); - eventWriter.flush(); - } + try { + XMLEventReader eventReader = new com.fasterxml.aalto.stax.InputFactoryImpl() + .createXMLEventReader(pipedInputStream); + XMLEventWriter eventWriter = new com.fasterxml.aalto.stax.OutputFactoryImpl() + .createXMLEventWriter(outputStream); - eventReader.close(); + XMLEvent event = null; + while (eventReader.hasNext()) { + event = eventReader.nextEvent(); + event = eventTransformer.transform(event); + eventWriter.add(event); eventWriter.flush(); - eventWriter.close(); - outputStream.flush(); - outputStream.close(); - } catch (XMLStreamException | IOException e) { - throw new IllegalStateException(e); } - return true; - } + + eventReader.close(); + eventWriter.flush(); + eventWriter.close(); + outputStream.flush(); + outputStream.close(); + } catch (XMLStreamException | IOException e) { + throw new IllegalStateException(e); + } + return true; + } }; future = Executors.newSingleThreadExecutor().submit(output); } From bb6451dd91a8e380e777b3dce6fe2a25e6792a29 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sun, 21 Jan 2018 15:22:21 +0000 Subject: [PATCH 044/228] Switch back to openjdk7 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7fdf6ecf..3d066ab9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,3 @@ language: java -jdk: oraclejdk8 +jdk: openjdk7 script: mvn clean test From 33ccc7be47917cf216673acaca8969fa86f976c7 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sun, 21 Jan 2018 15:33:33 +0000 Subject: [PATCH 045/228] Adding a small amount of documentation --- .../org/linguafranca/xml/XmlInputStreamFilter.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/simple/src/main/java/org/linguafranca/xml/XmlInputStreamFilter.java b/simple/src/main/java/org/linguafranca/xml/XmlInputStreamFilter.java index a951f5cc..1fffe7f1 100644 --- a/simple/src/main/java/org/linguafranca/xml/XmlInputStreamFilter.java +++ b/simple/src/main/java/org/linguafranca/xml/XmlInputStreamFilter.java @@ -27,7 +27,7 @@ /** - * A filter to accept a stream, interpret as XML, allow transformation + * A input stream filter to accept a stream, interpret as XML, allow transformation * as XML then forward as a stream. * *

Although this means that the interpretation of the XML will happen @@ -47,7 +47,7 @@ public class XmlInputStreamFilter extends InputStream { private boolean done = false; - private InputStream inputStream; + private InputStream inputStream; // the underlying input stream to read from private XmlEventTransformer eventTransformer; public XmlInputStreamFilter(InputStream is, XmlEventTransformer transformer) throws XMLStreamException { @@ -76,6 +76,7 @@ private int get(byte[] b, int offset, int length) throws IOException { } int totalBytesRead = 0; int bytesRead; + // read bytes from the transformed inputstream while ((bytesRead = xmlInStream.read(b, offset, length)) < length && !done) { if (bytesRead == -1) { try { @@ -102,12 +103,17 @@ private void loadEvents() throws XMLStreamException { done = true; return; } - + // get the next xml input event XMLEvent event = xmlEventReader.nextEvent(); + // transform it event = eventTransformer.transform(event); + // reset ouput buffer xmlWriteStream.reset(); + // write event to buffer xmlEventWriter.add(event); + // flush the written bytes xmlEventWriter.flush(); + // create an input stream from the bytes created xmlInStream = new ByteArrayInputStream(xmlWriteStream.toByteArray()); } From c4683f34db920f34026e498ca7a508cb5a935a32 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sun, 21 Jan 2018 16:33:04 +0000 Subject: [PATCH 046/228] update POMs --- http/pom.xml | 9 +++++---- pom.xml | 17 +++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/http/pom.xml b/http/pom.xml index 216a3915..10ba85bf 100644 --- a/http/pom.xml +++ b/http/pom.xml @@ -5,7 +5,8 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT + ../pom.xml 4.0.0 @@ -15,7 +16,7 @@ org.linguafranca.pwdb KeePassJava2-simple - 2.1.2-SNAPSHOT + ${project.version} org.eclipse.jetty @@ -40,12 +41,12 @@ com.google.code.gson gson - 2.7 + ${gson.version} junit junit - 4.12 + ${junit.version} test diff --git a/pom.xml b/pom.xml index ed5b0202..6945c9a1 100644 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,14 @@ UTF-8 1.7 + 3.3 + 2.19 + 1.6.7 + 2.4 + 2.10.3 + 1.6 + 2.5 + 19.0 1.10 1.54.0.0 @@ -77,14 +85,7 @@ 4.12 1.1.7 1.7.21 - - 3.3 - 2.19 - 1.6.7 - 2.4 - 2.10.3 - 1.6 - 2.5 + 2.7 From f0fa3e5c1e80b9b2eb9f7f617ee16afcaed33293 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sun, 21 Jan 2018 16:33:24 +0000 Subject: [PATCH 047/228] Update readme --- readme.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 5f1cf5ab..b6b215f3 100644 --- a/readme.md +++ b/readme.md @@ -34,7 +34,7 @@ The composite POM is org.linguafranca.pwdb KeePassJava2 - 2.1.2 + 2.1.3 at Maven Central. Note that the artifactId has become Camel Case from release 2.1.x onwards. @@ -170,8 +170,15 @@ Each module corresponds to a Maven artifact. The GroupId is `org.linguafranca.pw bindings might be useful for building other interfaces. domKeePassJava2-domA DOM based implementation of KDBX. Being DOM based it is rather slow, but messes less with existing content than the other two implementations. Known to work on Android. -httpkeepasshttpAn implementation of keepasshttp, which is a plugin for Windows Keepass -supporting communication with Chrome (chromeIPass) and Firefox(PassIFox) extensions. It is experimental and usafe. Please +httpkeepasshttpAn implementation of a server +intended to be the equivalent of +keepasshttp, which is a plugin for Windows Keepass +supporting communication with Chrome (chromeIPass) and Firefox(PassIFox) extensions. + + +**It is experimental and usafe.** + +Please read and inwardly digest the readme. From 7f23fa39c457c3dd594334f68a3d6ff11b0d2e03 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sun, 21 Jan 2018 16:54:44 +0000 Subject: [PATCH 048/228] [maven-release-plugin] prepare release KeePassJava2-2.1.3 --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- http/pom.xml | 6 ++---- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 4 ++-- simple/pom.xml | 2 +- test/pom.xml | 2 +- 11 files changed, 13 insertions(+), 15 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index 54839683..076dbdec 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3-SNAPSHOT + 2.1.3 ../pom.xml 4.0.0 diff --git a/database/pom.xml b/database/pom.xml index 2056e55e..bfdf37d0 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3-SNAPSHOT + 2.1.3 ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index 7cf15980..66a66e80 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3-SNAPSHOT + 2.1.3 ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index 75b4875d..e24ae241 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3-SNAPSHOT + 2.1.3 ../pom.xml 4.0.0 diff --git a/http/pom.xml b/http/pom.xml index 10ba85bf..06bce4ef 100644 --- a/http/pom.xml +++ b/http/pom.xml @@ -1,11 +1,9 @@ - + KeePassJava2-parent org.linguafranca.pwdb - 2.1.3-SNAPSHOT + 2.1.3 ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 42048428..449ab7bc 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3-SNAPSHOT + 2.1.3 ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index 1f8d7e8a..940a0001 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3-SNAPSHOT + 2.1.3 ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index d04bb908..1f270eb2 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3-SNAPSHOT + 2.1.3 ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index 6945c9a1..73a2e5b0 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.1.3-SNAPSHOT + 2.1.3 database test @@ -51,7 +51,7 @@ scm:git:git://github.com/jorabin/KeePassJava2.git scm:git:git@github.com:jorabin/KeePassJava2.git https://github.com/jorabin/KeePassJava2 - HEAD + KeePassJava2-2.1.3 diff --git a/simple/pom.xml b/simple/pom.xml index d53a5241..03d70a4a 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3-SNAPSHOT + 2.1.3 ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index 5db06d7d..8912051d 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3-SNAPSHOT + 2.1.3 4.0.0 From 89afc32d6ceda25bf29aca0871b99dedf2db24f3 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sun, 21 Jan 2018 16:54:50 +0000 Subject: [PATCH 049/228] [maven-release-plugin] prepare for next development iteration --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- http/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 4 ++-- simple/pom.xml | 2 +- test/pom.xml | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index 076dbdec..03dbb080 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3 + 2.1.4-SNAPSHOT ../pom.xml 4.0.0 diff --git a/database/pom.xml b/database/pom.xml index bfdf37d0..6cff12ec 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3 + 2.1.4-SNAPSHOT ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index 66a66e80..fd8cd5bb 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3 + 2.1.4-SNAPSHOT ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index e24ae241..f0a94c94 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3 + 2.1.4-SNAPSHOT ../pom.xml 4.0.0 diff --git a/http/pom.xml b/http/pom.xml index 06bce4ef..39a8bad3 100644 --- a/http/pom.xml +++ b/http/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3 + 2.1.4-SNAPSHOT ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 449ab7bc..cfb77998 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3 + 2.1.4-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index 940a0001..f3d5e49c 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3 + 2.1.4-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index 1f270eb2..81458cdd 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3 + 2.1.4-SNAPSHOT ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index 73a2e5b0..38d666f4 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.1.3 + 2.1.4-SNAPSHOT database test @@ -51,7 +51,7 @@ scm:git:git://github.com/jorabin/KeePassJava2.git scm:git:git@github.com:jorabin/KeePassJava2.git https://github.com/jorabin/KeePassJava2 - KeePassJava2-2.1.3 + HEAD diff --git a/simple/pom.xml b/simple/pom.xml index 03d70a4a..65975cdd 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3 + 2.1.4-SNAPSHOT ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index 8912051d..f51be205 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.3 + 2.1.4-SNAPSHOT 4.0.0 From e4f2606cdcf8bb9626102d4e95077bf80306d7c2 Mon Sep 17 00:00:00 2001 From: August Nagro Date: Mon, 22 Jan 2018 12:26:11 -0600 Subject: [PATCH 050/228] added Automatic Module names --- database/pom.xml | 17 +++++++++++++++++ dom/pom.xml | 16 ++++++++++++++++ jaxb/pom.xml | 12 ++++++++++++ kdb/pom.xml | 17 +++++++++++++++++ kdbx/pom.xml | 17 +++++++++++++++++ pom.xml | 1 + simple/pom.xml | 16 ++++++++++++++++ 7 files changed, 96 insertions(+) diff --git a/database/pom.xml b/database/pom.xml index 2056e55e..68dfaeb7 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -25,4 +25,21 @@ ${spongycastle.core.version} + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + org.linguafranca.pwdb.database + + + + + + \ No newline at end of file diff --git a/dom/pom.xml b/dom/pom.xml index 7cf15980..af85d50a 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -31,4 +31,20 @@ + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + org.linguafranca.pwdb.kdbx.dom + + + + + + \ No newline at end of file diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 42048428..adb37346 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -60,6 +60,18 @@ src/generated/java + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + org.linguafranca.pwdb.kdbx.jaxb + + + + diff --git a/kdb/pom.xml b/kdb/pom.xml index 1f8d7e8a..e0087e7f 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -32,4 +32,21 @@ + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + org.linguafranca.pwdb.kdb + + + + + + + \ No newline at end of file diff --git a/kdbx/pom.xml b/kdbx/pom.xml index d04bb908..0e168f95 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -68,4 +68,21 @@ + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + org.linguafranca.pwdb.kdbx + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 6945c9a1..01b12b35 100644 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,7 @@ 2.10.3 1.6 2.5 + 3.0.2 19.0 1.10 diff --git a/simple/pom.xml b/simple/pom.xml index d53a5241..26f26fbc 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -63,5 +63,21 @@ + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + org.linguafranca.pwdb.kdbx.simple + + + + + + \ No newline at end of file From 9f40ee86773d47611b1dbe5d9dbc4f605d6eb446 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 23 Jan 2018 17:10:28 +0000 Subject: [PATCH 051/228] Add a Maven Badge --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index b6b215f3..5ec74f75 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,6 @@ # KeePassJava2 +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2) Master: [![Build Status](https://travis-ci.org/jorabin/KeePassJava2.svg?branch=master)](https://travis-ci.org/jorabin/KeePassJava2) Develop: [![Build Status](https://travis-ci.org/jorabin/KeePassJava2.svg?branch=develop)](https://travis-ci.org/jorabin/KeePassJava2) A Java 7 API for databases compatible with the renowned [KeePass](http://keepass.info) password From dcc3dabfad3ced403303f7c77a4857b3333c8249 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 23 Jan 2018 17:18:31 +0000 Subject: [PATCH 052/228] maven badge with correct link --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 5ec74f75..115d5fc2 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # KeePassJava2 -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent) Master: [![Build Status](https://travis-ci.org/jorabin/KeePassJava2.svg?branch=master)](https://travis-ci.org/jorabin/KeePassJava2) Develop: [![Build Status](https://travis-ci.org/jorabin/KeePassJava2.svg?branch=develop)](https://travis-ci.org/jorabin/KeePassJava2) A Java 7 API for databases compatible with the renowned [KeePass](http://keepass.info) password From b9232a4a77babea6c6c8f193eff97ba7698212ba Mon Sep 17 00:00:00 2001 From: August Nagro Date: Tue, 23 Jan 2018 14:51:13 -0600 Subject: [PATCH 053/228] added expiration time setters --- .../src/main/java/org/linguafranca/pwdb/Entry.java | 8 ++++++++ .../linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java | 12 ++++++++++++ .../org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java | 11 +++++++++++ .../java/org/linguafranca/pwdb/kdb/KdbEntry.java | 11 +++++++++-- .../linguafranca/pwdb/kdbx/simple/SimpleEntry.java | 11 +++++++++++ 5 files changed, 51 insertions(+), 2 deletions(-) diff --git a/database/src/main/java/org/linguafranca/pwdb/Entry.java b/database/src/main/java/org/linguafranca/pwdb/Entry.java index a17cef26..fe49a2ec 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Entry.java +++ b/database/src/main/java/org/linguafranca/pwdb/Entry.java @@ -297,7 +297,15 @@ interface Matcher { boolean getExpires(); + void setExpires(boolean expires); + Date getExpiryTime(); + /** + * Sets the expiration date of this element. See {@link org.linguafranca.pwdb.Entry#setExpires(boolean)} + * @throws IllegalArgumentException if expiryTime is null. + */ + void setExpiryTime(Date expiryTime) throws IllegalArgumentException; + Date getLastModificationTime(); } diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java index f13555bb..8caee2c3 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java @@ -180,6 +180,11 @@ public boolean getExpires() { return content != null && content.equalsIgnoreCase("true"); } + @Override + public void setExpires(boolean expires) { + DomHelper.setElementContent(DomHelper.EXPIRES_ELEMENT_NAME, element, expires ? "True" : "False"); + } + @Override public Date getExpiryTime() { try { @@ -189,6 +194,13 @@ public Date getExpiryTime() { } } + @Override + public void setExpiryTime(Date expiryTime) throws IllegalArgumentException { + if (expiryTime == null) throw new IllegalArgumentException("expiryTime may not be null"); + String formatted = DomHelper.dateFormatter.format(expiryTime); + DomHelper.setElementContent(DomHelper.EXPIRY_TIME_ELEMENT_NAME, element, formatted); + } + @Override public Date getLastModificationTime() { try { diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java index 17e61ccd..3fb7e85b 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java @@ -219,11 +219,22 @@ public boolean getExpires() { return delegate.getTimes().getExpires(); } + @Override + public void setExpires(boolean expires) { + delegate.getTimes().setExpires(true); + } + @Override public Date getExpiryTime() { return delegate.getTimes().getExpiryTime(); } + @Override + public void setExpiryTime(Date expiryTime) throws IllegalArgumentException { + if (expiryTime == null) throw new IllegalArgumentException("expiryTime may not be null"); + delegate.getTimes().setExpiryTime(expiryTime); + } + @Override public Date getLastModificationTime() { return delegate.getTimes().getLastModificationTime(); diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java index cd832f83..e6b90aa9 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java @@ -43,6 +43,7 @@ public class KdbEntry extends AbstractEntry getBinaryPropertyNames() { throw new UnsupportedOperationException(); } + @Override + public void setExpires(boolean expires) { + this.expires = expires; + } + @Override public boolean getExpires() { - return false; + return expires; } @Override diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java index a7157fef..6f2b16d5 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java @@ -219,11 +219,22 @@ public boolean getExpires() { return times.getExpires(); } + @Override + public void setExpires(boolean expires) { + times.setExpires(expires); + } + @Override public Date getExpiryTime() { return times.getExpiryTime(); } + @Override + public void setExpiryTime(Date expiryTime) throws IllegalArgumentException { + if (expiryTime == null) throw new IllegalArgumentException("expiryTime may not be null"); + times.setExpiryTime(expiryTime); + } + @Override public Date getLastModificationTime() { return times.getLastModificationTime(); From d59ff5a7e0b43d9f97691286036b6e787d52a5f9 Mon Sep 17 00:00:00 2001 From: August Nagro Date: Tue, 23 Jan 2018 21:39:47 -0600 Subject: [PATCH 054/228] added Entry::removeProperty, Entry::removeBinaryProperty --- .../java/org/linguafranca/pwdb/Entry.java | 19 +++++++++ .../pwdb/kdbx/dom/DomEntryWrapper.java | 15 +++++++ .../linguafranca/pwdb/kdbx/dom/DomHelper.java | 12 +++++- .../pwdb/kdbx/jaxb/JaxbEntry.java | 39 +++++++++++++++++++ .../org/linguafranca/pwdb/kdb/KdbEntry.java | 10 +++++ .../pwdb/kdbx/simple/SimpleEntry.java | 26 +++++++++++++ 6 files changed, 120 insertions(+), 1 deletion(-) diff --git a/database/src/main/java/org/linguafranca/pwdb/Entry.java b/database/src/main/java/org/linguafranca/pwdb/Entry.java index a17cef26..04e1daaa 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Entry.java +++ b/database/src/main/java/org/linguafranca/pwdb/Entry.java @@ -113,6 +113,16 @@ interface Matcher { */ void setProperty(String name, String value); + /** + * Removes this non-standard property, if it exists. + * + * @return true if the property exists and was removed, false otherwise + * @param name the value of the property to remove + * @throws UnsupportedOperationException if non-standard properties are not supported + * @throws IllegalArgumentException if name is a standard property + */ + boolean removePropery(String name) throws IllegalArgumentException, UnsupportedOperationException; + /** * Returns a list of property names known to the entry. * @@ -143,6 +153,15 @@ interface Matcher { */ void setBinaryProperty(String name, byte[] value); + /** + * Removes this binary property, if it exists. + * + * @return true if the property was removed, false otherwise + * @param name the value of the property to remove + * @throws UnsupportedOperationException if non-standard properties are not supported + */ + boolean removeBinaryProperty(String name) throws UnsupportedOperationException; + /** * Returns a list of binary property names known to the entry. * diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java index f13555bb..9d347039 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java @@ -80,6 +80,14 @@ public void setProperty(String name, String value) { database.setDirty(true); } + @Override + public boolean removePropery(String name) throws IllegalArgumentException { + if (STANDARD_PROPERTY_NAMES.contains(name)) throw new IllegalArgumentException("may not remove property: " + name); + boolean wasRemoved = DomHelper.removeElement(String.format(DomHelper.PROPERTY_ELEMENT_FORMAT, name), element); + if (wasRemoved) database.setDirty(true); + return wasRemoved; + } + @Override public List getPropertyNames() { ArrayList result = new ArrayList<>(); @@ -112,6 +120,13 @@ public void setBinaryProperty(String name, byte[] value) { } + @Override + public boolean removeBinaryProperty(String name) { + boolean wasRemoved = DomHelper.removeElement(String.format(DomHelper.BINARY_PROPERTY_ELEMENT_FORMAT, name), element); + if (wasRemoved) database.setDirty(true); + return wasRemoved; + } + @Override public List getBinaryPropertyNames() { ArrayList result = new ArrayList<>(); diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java index f6ff4c39..51d8da96 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java @@ -120,6 +120,16 @@ static Element getElement(String elementPath, Element parentElement, boolean cr } } + static boolean removeElement(String elementPath, Element parentElement) { + Element toRemove = getElement(elementPath, parentElement, false); + if (toRemove == null) { + return false; + } else { + toRemove.getParentNode().removeChild(toRemove); + return true; + } + } + static List getElements (String elementPath, Element parentElement) { try { NodeList nodes = (NodeList) xpath.evaluate(elementPath, parentElement, XPathConstants.NODESET); @@ -142,6 +152,7 @@ static int getElementsCount (String elementPath, Element parentElement) { } } + @Nullable static Element newElement(String elementName, Element parentElement) { Element newElement = parentElement.getOwnerDocument().createElement(elementName); parentElement.appendChild(newElement); @@ -214,7 +225,6 @@ static Element setBinaryElementContent(String elementPath, Element parentElement } } - @NotNull static Element touchElement(String elementPath, Element parentElement) { return setElementContent(elementPath, parentElement, dateFormatter.format(new Date())); diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java index 17e61ccd..c922c5e1 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java @@ -103,6 +103,26 @@ public void setProperty(String name, String value) { touch(); } + @Override + public boolean removePropery(String name) throws IllegalArgumentException { + if (STANDARD_PROPERTY_NAMES.contains(name)) throw new IllegalArgumentException("may not remove property: " + name); + + StringField toRemove = null; + for (StringField field: delegate.getString()){ + if (field.getKey().equals(name)) { + toRemove = field; + break; + } + } + if (toRemove == null) { + return false; + } else { + delegate.getString().remove(toRemove); + touch(); + return true; + } + } + @Override public List getPropertyNames() { List result = new ArrayList<>(); @@ -171,6 +191,25 @@ public void setBinaryProperty(String name, byte[] value) { touch(); } + @Override + public boolean removeBinaryProperty(String name) throws UnsupportedOperationException { + BinaryField toRemove = null; + for (BinaryField binaryField : delegate.getBinary()) { + if (binaryField.getKey().equals(name)) { + toRemove = binaryField; + break; + } + } + + if (toRemove == null) { + return false; + } else { + delegate.getBinary().remove(toRemove); + touch(); + return true; + } + } + @Override public List getBinaryPropertyNames() { List result = new ArrayList<>(); diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java index cd832f83..46d64c74 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java @@ -71,6 +71,16 @@ public void setProperty(String name, String value) { } } + @Override + public boolean removePropery(String name) throws UnsupportedOperationException { + throw new UnsupportedOperationException("Cannot remove non-standard properties in KDB format"); + } + + @Override + public boolean removeBinaryProperty(String name) throws UnsupportedOperationException { + throw new UnsupportedOperationException("Cannot remove binary properties in KDB format"); + } + @Override public List getPropertyNames() { return new ArrayList<>(Entry.STANDARD_PROPERTY_NAMES); diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java index a7157fef..4cf9b308 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java @@ -113,6 +113,20 @@ public void setProperty(String s, String s1) { touch(); } + @Override + public boolean removePropery(String name) throws IllegalArgumentException { + if (STANDARD_PROPERTY_NAMES.contains(name)) throw new IllegalArgumentException("may not remove property: " + name); + + EntryClasses.StringProperty sp = getStringProperty(name, string); + if (sp == null) { + return false; + } else { + this.string.remove(sp); + touch(); + return true; + } + } + @Override public List getPropertyNames() { List result = new ArrayList<>(); @@ -175,6 +189,18 @@ public void setBinaryProperty(String s, byte[] bytes) { touch(); } + @Override + public boolean removeBinaryProperty(String name) throws UnsupportedOperationException { + BinaryProperty bp = getBinaryProp(name, binary); + if (bp == null) { + return true; + } else { + binary.remove(bp); + touch(); + return true; + } + } + @Override public List getBinaryPropertyNames() { List result = new ArrayList<>(); From b79f973b9336c140fd9605d9be3e053409f53e42 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Thu, 25 Jan 2018 18:30:38 +0000 Subject: [PATCH 055/228] Tidy POMs. Add tests for new functionality and times. Correct new functionality and times. --- CHANGELOG.md | 11 ++- all/pom.xml | 1 + database/pom.xml | 8 +- .../java/org/linguafranca/pwdb/Database.java | 33 ++++++-- .../java/org/linguafranca/pwdb/Entry.java | 50 ++++++++++-- .../pwdb/base/AbstractDatabase.java | 15 ++++ dom/pom.xml | 2 - .../pwdb/kdbx/dom/DomDatabaseWrapper.java | 2 + .../pwdb/kdbx/dom/DomEntryWrapper.java | 2 +- .../pwdb/kdbx/dom/DomEntryWrapperTest.java | 4 +- http/pom.xml | 5 -- jaxb/pom.xml | 2 - .../pwdb/kdbx/jaxb/JaxbEntry.java | 4 +- .../pwdb/kdbx/jaxb/JaxbEntryTest.java | 4 +- kdb/pom.xml | 10 +-- .../linguafranca/pwdb/kdb/KdbDatabase.java | 15 ++++ .../org/linguafranca/pwdb/kdb/KdbEntry.java | 8 +- .../pwdb/kdb/KdbDatabaseTest.java | 10 +++ kdbx/pom.xml | 10 --- pom.xml | 81 +++++++++++++++---- simple/pom.xml | 2 - .../pwdb/kdbx/simple/SimpleEntry.java | 12 +-- .../pwdb/kdbx/simple/model/Times.java | 2 +- .../pwdb/kdbx/simple/SimpleEntryTest.java | 4 +- test/pom.xml | 4 - .../pwdb/checks/BasicDatabaseChecks.java | 53 +++++++++++- ...yChecks.java => BinaryPropertyChecks.java} | 29 ++++++- 27 files changed, 294 insertions(+), 89 deletions(-) rename test/src/main/java/org/linguafranca/pwdb/checks/{EntryChecks.java => BinaryPropertyChecks.java} (72%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f273910..c78f106b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,16 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.com) and [Semantic Versioning](http://semver.org/spec/v2.0.0.html) -##[2.1.3] +##[2.1.4-Snapshot] + +### Added + +- removeProperty for custom property via @AugustNagro +- AutomaticModuleNames for Java 9 via @AugustNagro +- expires functionality on Entry via @AugustNagro +- database reports support for optional features + +##[2.1.3] 2018-01-21 ### Fixed diff --git a/all/pom.xml b/all/pom.xml index 03dbb080..3ac4774c 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -22,6 +22,7 @@ 2.1.4-SNAPSHOT ../pom.xml + 4.0.0 KeePassJava2 diff --git a/database/pom.xml b/database/pom.xml index 5b0d8225..129571ea 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -13,16 +13,17 @@ Contains root definitions of PWDB Databases etc. + + org.jetbrains + annotations + com.google.guava guava - ${guava.version} - com.madgag.spongycastle core - ${spongycastle.core.version} @@ -31,7 +32,6 @@ org.apache.maven.plugins maven-jar-plugin - ${maven-jar-plugin.version} diff --git a/database/src/main/java/org/linguafranca/pwdb/Database.java b/database/src/main/java/org/linguafranca/pwdb/Database.java index 7f0ed064..67295b56 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Database.java +++ b/database/src/main/java/org/linguafranca/pwdb/Database.java @@ -16,7 +16,7 @@ package org.linguafranca.pwdb; -import org.linguafranca.pwdb.Credentials; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.OutputStream; @@ -118,7 +118,7 @@ public interface Database , G extends Group, G extends Group, G extends Group, G extends Group, G extends Groupname is a standard property + * @see Database#supportsNonStandardPropertyNames() */ - boolean removePropery(String name) throws IllegalArgumentException, UnsupportedOperationException; + boolean removeProperty(String name) throws IllegalArgumentException, UnsupportedOperationException; /** * Returns a list of property names known to the entry. @@ -139,6 +144,7 @@ interface Matcher { * * @param name the name of the property to get * @return a value or null if the property is not known, or if setting of arbitrary properties is not supported + * @see Database#supportsBinaryProperties */ byte[] getBinaryProperty(String name); @@ -149,7 +155,8 @@ interface Matcher { * * @param name the name of the property to set * @param value the value to set it to - * @throws UnsupportedOperationException if binary properties are not supported. + * @throws UnsupportedOperationException if binary properties are not supported + * @see Database#supportsBinaryProperties() */ void setBinaryProperty(String name, byte[] value); @@ -158,7 +165,8 @@ interface Matcher { * * @return true if the property was removed, false otherwise * @param name the value of the property to remove - * @throws UnsupportedOperationException if non-standard properties are not supported + * @throws UnsupportedOperationException if binary properties are not supported + * @see Database#supportsBinaryProperties() */ boolean removeBinaryProperty(String name) throws UnsupportedOperationException; @@ -168,7 +176,8 @@ interface Matcher { *

All implementations of Entry are required to support reading and writing of * {@link #STANDARD_PROPERTY_NAMES}. * @return a list that is modifiable by the caller without affecting the Entry. - * @throws UnsupportedOperationException if binary properties are not supported. + * @throws UnsupportedOperationException if binary properties are not supported + * @see Database#supportsBinaryProperties() */ List getBinaryPropertyNames(); @@ -176,7 +185,7 @@ interface Matcher { * Get the parent of this entry * @return a parent */ - G getParent(); + @NotNull G getParent(); /** * Get the UUID of this entry. Databases (like KDB) that do not natively support @@ -184,7 +193,7 @@ interface Matcher { * * @return a UUID */ - UUID getUuid(); + @NotNull UUID getUuid(); /** * Get the username field of this entry @@ -310,21 +319,46 @@ interface Matcher { */ void setIcon(I icon); + /** + * Returns the date at which any value was retrieved from this entry. + *

+ * Implementations SHOULD set this to the creation date or earlier if the entry has never been used. + */ Date getLastAccessTime(); + /** + * Returns the date at which this entry was created + */ Date getCreationTime(); + /** + * Returns true if this entry is to be considered as expired at some point + */ boolean getExpires(); + /** + * Set true for the date returned by {@link #getExpiryTime()} to be considered an expiry time + * @see #setExpiryTime(Date) + */ void setExpires(boolean expires); + /** + * Returns a date at which the entry should be considered to have expired, if {@link #getExpires()} is true - + * otherwise returns an arbitrary date. + */ Date getExpiryTime(); /** - * Sets the expiration date of this element. See {@link org.linguafranca.pwdb.Entry#setExpires(boolean)} + * Sets the expiry date of this element. * @throws IllegalArgumentException if expiryTime is null. + * @see org.linguafranca.pwdb.Entry#setExpires(boolean) */ void setExpiryTime(Date expiryTime) throws IllegalArgumentException; + /** + * Returns the date that the entry was last modified + *

+ * Implementations SHOULD set this to the creation date or earlier if the entry has never been used. + */ Date getLastModificationTime(); } diff --git a/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java b/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java index ebe34be9..fd542a69 100644 --- a/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java +++ b/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java @@ -217,4 +217,19 @@ public void emptyRecycleBin() { recycle.removeEntry(e); } } + + @Override + public boolean supportsNonStandardPropertyNames() { + return true; + } + + @Override + public boolean supportsBinaryProperties() { + return true; + } + + @Override + public boolean supportsRecycleBin() { + return true; + } } diff --git a/dom/pom.xml b/dom/pom.xml index d0f0c3e9..f35731a2 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -26,7 +26,6 @@ junit junit - ${junit.version} test @@ -36,7 +35,6 @@ org.apache.maven.plugins maven-jar-plugin - ${maven-jar-plugin.version} diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java index f3389a7a..1ea94d15 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java @@ -177,4 +177,6 @@ public void setDescription(String description) { DomHelper.touchElement("DatabaseDescriptionChanged", dbMeta); setDirty(true); } + + } diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java index 43b70051..16d8949e 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java @@ -81,7 +81,7 @@ public void setProperty(String name, String value) { } @Override - public boolean removePropery(String name) throws IllegalArgumentException { + public boolean removeProperty(String name) throws IllegalArgumentException { if (STANDARD_PROPERTY_NAMES.contains(name)) throw new IllegalArgumentException("may not remove property: " + name); boolean wasRemoved = DomHelper.removeElement(String.format(DomHelper.PROPERTY_ELEMENT_FORMAT, name), element); if (wasRemoved) database.setDirty(true); diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapperTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapperTest.java index 0f4095b5..c2b776dc 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapperTest.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapperTest.java @@ -16,7 +16,7 @@ package org.linguafranca.pwdb.kdbx.dom; -import org.linguafranca.pwdb.checks.EntryChecks; +import org.linguafranca.pwdb.checks.BinaryPropertyChecks; import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.Credentials; @@ -28,7 +28,7 @@ /** * @author jo */ -public class DomEntryWrapperTest extends EntryChecks { +public class DomEntryWrapperTest extends BinaryPropertyChecks { public DomEntryWrapperTest () throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment.kdbx"); diff --git a/http/pom.xml b/http/pom.xml index 39a8bad3..a6fff8d3 100644 --- a/http/pom.xml +++ b/http/pom.xml @@ -24,27 +24,22 @@ org.slf4j slf4j-api - ${slf4j-api.version} ch.qos.logback logback-core - ${logback.version} ch.qos.logback logback-classic - ${logback.version} com.google.code.gson gson - ${gson.version} junit junit - ${junit.version} test diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 217ad0e7..3acc7338 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -43,7 +43,6 @@ junit junit - ${junit.version} test @@ -63,7 +62,6 @@ org.apache.maven.plugins maven-jar-plugin - ${maven-jar-plugin.version} diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java index 0698532c..eb8391d0 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java @@ -51,7 +51,7 @@ public JaxbEntry(JaxbDatabase jaxbDatabase) { delegate.getString().add(field); } - Date now = new Date(); + Date now = new Date(System.currentTimeMillis() / 1000L * 1000L); // to nearest lower second Times times = new Times(); times.setLastModificationTime(now); times.setCreationTime(now); @@ -104,7 +104,7 @@ public void setProperty(String name, String value) { } @Override - public boolean removePropery(String name) throws IllegalArgumentException { + public boolean removeProperty(String name) throws IllegalArgumentException { if (STANDARD_PROPERTY_NAMES.contains(name)) throw new IllegalArgumentException("may not remove property: " + name); StringField toRemove = null; diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntryTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntryTest.java index 46763a43..f967b8f2 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntryTest.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntryTest.java @@ -17,7 +17,7 @@ package org.linguafranca.pwdb.kdbx.jaxb; import org.linguafranca.pwdb.Database; -import org.linguafranca.pwdb.checks.EntryChecks; +import org.linguafranca.pwdb.checks.BinaryPropertyChecks; import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.Credentials; @@ -28,7 +28,7 @@ /** * @author jo */ -public class JaxbEntryTest extends EntryChecks { +public class JaxbEntryTest extends BinaryPropertyChecks { public JaxbEntryTest() { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment.kdbx"); diff --git a/kdb/pom.xml b/kdb/pom.xml index a831634e..b1ce06f1 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -18,18 +18,16 @@ database ${project.version} + + org.jetbrains + annotations + org.linguafranca.pwdb test ${project.version} test - - junit - junit - ${junit.version} - test - diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbDatabase.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbDatabase.java index b6733830..30c1295f 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbDatabase.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbDatabase.java @@ -157,4 +157,19 @@ public void enableRecycleBin(boolean enable) { public KdbGroup getRecycleBin() { return null; } + + @Override + public boolean supportsNonStandardPropertyNames() { + return false; + } + + @Override + public boolean supportsBinaryProperties() { + return false; + } + + @Override + public boolean supportsRecycleBin() { + return false; + } } diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java index 73a0813e..d7440b56 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java @@ -40,9 +40,9 @@ public class KdbEntry extends AbstractEntry commons-codec commons-codec - ${commons-codec.version} org.jetbrains annotations - ${annotations.version} - - - com.intellij - annotations - ${annotations.intellij.version} - provided org.linguafranca.pwdb @@ -63,7 +55,6 @@ junit junit - ${junit.version} test @@ -73,7 +64,6 @@ org.apache.maven.plugins maven-jar-plugin - ${maven-jar-plugin.version} diff --git a/pom.xml b/pom.xml index a9b9d88c..fbca0744 100644 --- a/pom.xml +++ b/pom.xml @@ -77,19 +77,24 @@ 1.6 2.5 3.0.2 - - 19.0 - 1.10 - 1.54.0.0 - 15.0 - 12.0 - 4.12 - 1.1.7 - 1.7.21 - 2.7 + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + org.apache.maven.plugins @@ -100,11 +105,6 @@ ${java.version} - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - org.apache.maven.plugins maven-release-plugin @@ -119,6 +119,57 @@ + + + + org.jetbrains + annotations + 15.0 + + + com.google.guava + guava + 19.0 + + + + com.madgag.spongycastle + core + 1.54.0.0 + + + com.google.code.gson + gson + 2.7 + + + org.slf4j + slf4j-api + 1.7.21 + + + ch.qos.logback + logback-core + 1.1.7 + + + ch.qos.logback + logback-classic + 1.1.7 + + + commons-codec + commons-codec + 1.10 + + + junit + junit + 4.12 + + + + signJar diff --git a/simple/pom.xml b/simple/pom.xml index 4000f8a4..c17da12c 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -43,7 +43,6 @@ junit junit - ${junit.version} test @@ -68,7 +67,6 @@ org.apache.maven.plugins maven-jar-plugin - ${maven-jar-plugin.version} diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java index 3aaf84e6..3fea86c5 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java @@ -78,7 +78,7 @@ public class SimpleEntry extends AbstractEntry(); binary = new ArrayList<>(); - times = new Times(new Date()); + times = new Times(); uuid = UUID.randomUUID(); iconID = 0; } @@ -92,8 +92,9 @@ public static SimpleEntry createEntry(SimpleDatabase database) { SimpleEntry result = new SimpleEntry(); result.database = database; result.parent = null; + // avoiding setProperty as it does a touch(); for (String p: STANDARD_PROPERTY_NAMES) { - result.setProperty(p, ""); + result.string.add(new EntryClasses.StringProperty(p, new EntryClasses.StringProperty.Value(""))); } return result; } @@ -114,7 +115,7 @@ public void setProperty(String s, String s1) { } @Override - public boolean removePropery(String name) throws IllegalArgumentException { + public boolean removeProperty(String name) throws IllegalArgumentException { if (STANDARD_PROPERTY_NAMES.contains(name)) throw new IllegalArgumentException("may not remove property: " + name); EntryClasses.StringProperty sp = getStringProperty(name, string); @@ -192,13 +193,12 @@ public void setBinaryProperty(String s, byte[] bytes) { @Override public boolean removeBinaryProperty(String name) throws UnsupportedOperationException { BinaryProperty bp = getBinaryProp(name, binary); - if (bp == null) { - return true; - } else { + if (bp != null) { binary.remove(bp); touch(); return true; } + return false; } @Override diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/Times.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/Times.java index 388b345f..b96388e4 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/Times.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/Times.java @@ -108,7 +108,7 @@ public void setLocationChanged(Date locationChanged) { } public Times() { - this(new Date()); + this(new Date(System.currentTimeMillis()/1000*1000)); } public Times(Date date) { lastModificationTime = date; diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntryTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntryTest.java index 96b7f384..b8cd5f00 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntryTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntryTest.java @@ -16,7 +16,7 @@ package org.linguafranca.pwdb.kdbx.simple; -import org.linguafranca.pwdb.checks.EntryChecks; +import org.linguafranca.pwdb.checks.BinaryPropertyChecks; import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.Credentials; @@ -28,7 +28,7 @@ /** * @author jo */ -public class SimpleEntryTest extends EntryChecks { +public class SimpleEntryTest extends BinaryPropertyChecks { public SimpleEntryTest() { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment.kdbx"); diff --git a/test/pom.xml b/test/pom.xml index f51be205..63bc46cb 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -20,22 +20,18 @@ junit junit - ${junit.version} org.slf4j slf4j-api - ${slf4j-api.version} ch.qos.logback logback-core - ${logback.version} ch.qos.logback logback-classic - ${logback.version} diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java index 4179bc41..38eb4e2a 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Date; import java.util.List; import static org.junit.Assert.*; @@ -34,7 +35,7 @@ */ public abstract class BasicDatabaseChecks , G extends Group, E extends Entry, I extends Icon> { - private Database database; + protected Database database; public abstract Database createDatabase() throws IOException; @@ -154,6 +155,7 @@ public void testSetFields () { Assert.assertEquals(0, properties.size()); } catch (UnsupportedOperationException e) { // databases don't have to support arbitrary properties + assertTrue(!database.supportsNonStandardPropertyNames()); assertArrayEquals(e1.getPropertyNames().toArray(), Entry.STANDARD_PROPERTY_NAMES.toArray()); } @@ -164,6 +166,55 @@ public void testSetFields () { assertEquals("Entry 2", e1.getPath()); } + @Test + public void testTimes() { + long before = (new Date().getTime() / 1000L) * 1000L; // round to next lower second + E entry = database.newEntry(); + long after = (new Date().getTime()/ 1000L) * 1000L; // round to next lower second + long created = entry.getCreationTime().getTime(); + assertTrue(created >= before && created <= after); + assertFalse(entry.getExpires()); + assertTrue(entry.getLastAccessTime().getTime() <= created); + assertTrue(entry.getLastModificationTime().getTime() <= created); + + entry.setExpires(true); + entry.setExpiryTime(new Date(created)); + + assertTrue(entry.getExpires()); + assertEquals(created, entry.getExpiryTime().getTime()); + + + } + + @Test + public void checkAddChangeRemoveProperty() { + // only applies to databases that support arbitrary properties + E entry = database.newEntry(); + assertEquals(Entry.STANDARD_PROPERTY_NAMES.size(), entry.getPropertyNames().size()); + try { + entry.setProperty("test", "test1"); + } catch (UnsupportedOperationException e) { + if (!database.supportsNonStandardPropertyNames()) { + return; + } + fail("Database must report that it doesn't support non standrad properties"); + } + assertEquals("test1", entry.getProperty("test")); + entry.setProperty("test", "test2"); + assertEquals("test2", entry.getProperty("test")); + assertTrue(entry.removeProperty("test")); + assertFalse(entry.removeProperty("test")); + assertFalse(entry.removeProperty("test-test")); + assertEquals(Entry.STANDARD_PROPERTY_NAMES.size(), entry.getPropertyNames().size()); + try { + entry.removeProperty(Entry.STANDARD_PROPERTY_NAME_USER_NAME); + fail("Should not be able to remove standard property"); + } catch (IllegalArgumentException ignore) { + // ignore as expected + } + } + + @Test public void testNewEntry() { E e2 = database.newEntry(); diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/EntryChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java similarity index 72% rename from test/src/main/java/org/linguafranca/pwdb/checks/EntryChecks.java rename to test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java index cbd20806..bb0adc03 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/EntryChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java @@ -29,13 +29,13 @@ import java.io.InputStream; import java.io.OutputStream; -import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.*; /** * @author jo */ -public abstract class EntryChecks { +public abstract class BinaryPropertyChecks { public Database database; @SuppressWarnings("unused") public abstract void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException; @@ -78,4 +78,29 @@ public void getBinaryPropertyNames() throws Exception { entry = database.findEntries("Test 2 attachment").get(0); assertArrayEquals(new String[] {"letter J.jpeg", "letter L.jpeg"}, entry.getBinaryPropertyNames().toArray()); } + + @Test + public void checkSupported(){ + assertTrue(database.supportsBinaryProperties()); + } + + @Test + public void checkAddChangeRemoveBinaryProperty() { + byte[] test = new byte[] {0, 1, 2 ,3}; + byte[] test2 = new byte[] {3, 2, 1, 0}; + Entry entry = database.findEntries("Test attachment").get(0); + assertEquals(1, entry.getBinaryPropertyNames().size()); + entry.setBinaryProperty("test", test); + assertArrayEquals(test, entry.getBinaryProperty("test")); + entry.setBinaryProperty("test", test2); + assertArrayEquals(test2, entry.getBinaryProperty("test")); + // true that property was removed + assertTrue(entry.removeBinaryProperty("test")); + // false that same property was removed + assertFalse(entry.removeBinaryProperty("test")); + // false that non existent was removed + assertFalse(entry.removeBinaryProperty("test-test")); + // same number of properties as we started with + assertEquals(1, entry.getBinaryPropertyNames().size()); + } } From 155fdb4e4fefc31a5c595a5a139f027fed8d9904 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 29 Jan 2018 13:44:58 +0000 Subject: [PATCH 056/228] Preliminary V4 file reading --- database/pom.xml | 11 + .../org/linguafranca/pwdb/Credentials.java | 10 +- .../org/linguafranca/pwdb/security/Aes.java | 52 ++++ .../org/linguafranca/pwdb/security/Argon.java | 68 +++++ .../linguafranca/pwdb/security/ChaCha.java | 14 + .../pwdb/security/Encryption.java | 47 ++- .../pwdb/security/VariantDictionary.java | 89 ++++++ .../pwdb/kdbx/dom/DomDatabaseWrapper.java | 3 +- .../kdbx/dom/DomSerializableDatabase.java | 3 +- .../pwdb/kdbx/dom/DomDatabaseWrapperTest.java | 2 +- .../pwdb/kdbx/dom/VisitorTest.java | 2 +- .../org/linguafranca/pwdb/kdbx/SaxParse.java | 6 +- .../pwdb/kdbx/jaxb/JaxbDatabase.java | 2 +- .../linguafranca/pwdb/kdb/KdbCredentials.java | 4 +- .../org/linguafranca/pwdb/kdb/KdbHeader.java | 5 +- .../linguafranca/pwdb/kdb/KdbSerializer.java | 3 +- .../hashedblock/HmacBlockInputStream.java | 98 +++++++ .../pwdb/kdbx/KdbxCredentials.java | 6 +- .../org/linguafranca/pwdb/kdbx/KdbxCreds.java | 6 +- .../kdbx/{stream_3_1 => }/KdbxHeader.java | 145 +++++++--- .../kdbx/{stream_3_1 => }/KdbxSerializer.java | 272 +++++++++++++----- .../{stream_3_1 => }/KdbxStreamFormat.java | 8 +- .../pwdb/kdbx/StreamEncryptor.java | 86 ++++++ .../stream_3_1/Salsa20StreamEncryptor.java | 94 ------ .../pwdb/kdbx/KdbxKeyFileTest.java | 3 +- .../pwdb/kdbx/KdbxSerializerTest.java | 2 - .../pwdb/kdbx/stream/KdbxSerializerTest.java | 87 ++++++ ...ionTest.java => StreamEncryptionTest.java} | 27 +- pom.xml | 2 +- .../pwdb/kdbx/simple/SimpleDatabase.java | 10 +- .../pwdb/kdbx/KdbxInnerInputStreamTest.java | 18 +- .../kdbx/simple/SimpleSaveAndReloadTest.java | 5 +- test/src/main/resources/V4-AES-AES.kdbx | Bin 0 -> 1966 bytes test/src/main/resources/V4-AES-Argon2.kdbx | Bin 0 -> 2005 bytes test/src/main/resources/V4-ChaCha-AES.kdbx | Bin 0 -> 1958 bytes 35 files changed, 906 insertions(+), 284 deletions(-) create mode 100644 database/src/main/java/org/linguafranca/pwdb/security/Aes.java create mode 100644 database/src/main/java/org/linguafranca/pwdb/security/Argon.java create mode 100644 database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java create mode 100644 database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java create mode 100644 kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java rename kdbx/src/main/java/org/linguafranca/pwdb/kdbx/{stream_3_1 => }/KdbxHeader.java (61%) rename kdbx/src/main/java/org/linguafranca/pwdb/kdbx/{stream_3_1 => }/KdbxSerializer.java (56%) rename kdbx/src/main/java/org/linguafranca/pwdb/kdbx/{stream_3_1 => }/KdbxStreamFormat.java (83%) delete mode 100644 kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/Salsa20StreamEncryptor.java create mode 100644 kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java rename kdbx/src/test/java/org/linguafranca/pwdb/security/{Salsa20EncryptionTest.java => StreamEncryptionTest.java} (66%) create mode 100755 test/src/main/resources/V4-AES-AES.kdbx create mode 100755 test/src/main/resources/V4-AES-Argon2.kdbx create mode 100755 test/src/main/resources/V4-ChaCha-AES.kdbx diff --git a/database/pom.xml b/database/pom.xml index 129571ea..d08c3303 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -25,6 +25,17 @@ com.madgag.spongycastle core + + com.kosprov.jargon2 + jargon2-api + 1.0.1 + + + com.kosprov.jargon2 + jargon2-native-ri-backend + 1.1.0 + runtime + diff --git a/database/src/main/java/org/linguafranca/pwdb/Credentials.java b/database/src/main/java/org/linguafranca/pwdb/Credentials.java index 0ef1d817..f09f475e 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Credentials.java +++ b/database/src/main/java/org/linguafranca/pwdb/Credentials.java @@ -16,8 +16,9 @@ package org.linguafranca.pwdb; +import org.linguafranca.pwdb.security.Encryption; + import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; /** * Supports a contract that yields a key for decryption of databases @@ -32,12 +33,7 @@ public interface Credentials { class None implements Credentials { @Override public byte[] getKey() { - MessageDigest md = null; - try { - md = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } + MessageDigest md = Encryption.getSha256MessageDigestInstance(); return md.digest(new byte[0]); } } diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java new file mode 100644 index 00000000..9f3f6561 --- /dev/null +++ b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java @@ -0,0 +1,52 @@ +package org.linguafranca.pwdb.security; + +import org.spongycastle.crypto.engines.AESEngine; +import org.spongycastle.crypto.modes.CBCBlockCipher; +import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.spongycastle.crypto.params.KeyParameter; + +import java.security.MessageDigest; +import java.util.UUID; + +import static org.linguafranca.pwdb.security.Encryption.getSha256MessageDigestInstance; + +/** + * @author jo + */ +public class Aes { + + public static final UUID KDF = UUID.fromString("C9D9F39A-628A-4460-BF74-0D08C18A4FEA"); + + public static class KdfKeys { + public static final String ParamRounds = "R"; // UInt64 + public static final String ParamSeed = "S"; // Byte[32] + } + + public static PaddedBufferedBlockCipher getCipher() { + return new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine())); + } + /** + * Create a final key from the parameters passed + */ + public static byte[] getFinalKeyDigest(byte[] key, byte[] masterSeed, byte[] transformSeed, long transformRounds) { + + AESEngine engine = new AESEngine(); + engine.init(true, new KeyParameter(transformSeed)); + + // copy input key + byte[] transformedKey = new byte[key.length]; + System.arraycopy(key, 0, transformedKey, 0, transformedKey.length); + + // transform rounds times + for (long rounds = 0; rounds < transformRounds; rounds++) { + engine.processBlock(transformedKey, 0, transformedKey, 0); + engine.processBlock(transformedKey, 16, transformedKey, 16); + } + + MessageDigest md = getSha256MessageDigestInstance(); + byte[] transformedKeyDigest = md.digest(transformedKey); + + md.update(masterSeed); + return md.digest(transformedKeyDigest); + } +} diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Argon.java b/database/src/main/java/org/linguafranca/pwdb/security/Argon.java new file mode 100644 index 00000000..389c9ece --- /dev/null +++ b/database/src/main/java/org/linguafranca/pwdb/security/Argon.java @@ -0,0 +1,68 @@ +package org.linguafranca.pwdb.security; + +import java.security.MessageDigest; +import java.util.UUID; + +import static com.kosprov.jargon2.api.Jargon2.*; +import static org.linguafranca.pwdb.security.Argon.ArgonParameterKeys.ArgonKeys.*; +import static org.linguafranca.pwdb.security.Encryption.getSha256MessageDigestInstance; + + +/** + * @author jo + */ +public class Argon { + + public static final UUID argon2_kdf = UUID.fromString("EF636DDF-8C29-444B-91F7-A9A403E30A0C"); + + public static class ArgonParameterKeys { + @SuppressWarnings("unused") + public static class ArgonKeys { + static final String paramSalt = "S"; // Byte[] + static final String paramParallelism = "P"; // UInt32 + static final String paramMemory = "M"; // UInt64 + static final String paramIterations = "I"; // UInt64 + static final String paramVersion = "V"; // UInt32 + static final String paramSecretKey = "K"; // Byte[] + static final String paramAssocData = "A"; // Byte[] + + static final int minVersion = 0x10; + static final int maxVersion = 0x13; + } + } + + @SuppressWarnings("ConstantConditions") + public static byte [] getArgonFinalKeyDigest(byte [] digest, byte [] masterSeed, VariantDictionary argonParameterKeys) { + + byte bVersion = argonParameterKeys.get(paramVersion).asByteArray()[0]; + Version version = bVersion == 0x13 ? Version.V13 : Version.V10; + byte [] salt = argonParameterKeys.get(paramSalt).asByteArray(); + int parallelism = argonParameterKeys.get(paramParallelism).asInteger(); + int memoryCost = (int) argonParameterKeys.get(paramMemory).asLong(); + int timeCost = (int) argonParameterKeys.get(paramIterations).asLong(); +/* + byte [] secretKey = argonParameterKeys.entries.get(paramSecretKey).value; + byte [] assocData = argonParameterKeys.entries.get(paramAssocData).value; +*/ + + // Configure the hasher + Hasher hasher = jargon2Hasher() + .type(Type.ARGON2d) + .version(version) + .salt(salt) + .parallelism(parallelism) + .memoryCost(memoryCost/1024) + .timeCost(timeCost) + .hashLength(32); + + + byte [] hash = hasher.password(digest).rawHash(); + + MessageDigest md = getSha256MessageDigestInstance(); + byte [] transformedKeyDigest = hash; // md.digest(hash); + + md.update(masterSeed); + return md.digest(transformedKeyDigest); + + } +} diff --git a/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java b/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java new file mode 100644 index 00000000..38f0b409 --- /dev/null +++ b/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java @@ -0,0 +1,14 @@ +package org.linguafranca.pwdb.security; + +import org.spongycastle.crypto.StreamCipher; +import org.spongycastle.crypto.engines.ChaCha7539Engine; + +/** + * @author jo + */ +public class ChaCha { + + public static StreamCipher getCipher () { + return new ChaCha7539Engine(); + } +} diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java b/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java index 8c4f6ee8..ed957d2c 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java @@ -17,7 +17,7 @@ package org.linguafranca.pwdb.security; // use spongycastle repackaging of bouncycastle in deference to Android needs -import org.spongycastle.crypto.engines.AESEngine; +import org.spongycastle.crypto.StreamCipher; import org.spongycastle.crypto.engines.AESFastEngine; import org.spongycastle.crypto.io.CipherInputStream; import org.spongycastle.crypto.io.CipherOutputStream; @@ -26,6 +26,7 @@ import org.spongycastle.crypto.params.KeyParameter; import org.spongycastle.crypto.params.ParametersWithIV; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; @@ -64,7 +65,7 @@ public static byte[] getDigest(String string, String encoding) { if (encoding == null || encoding.length() == 0) throw new IllegalArgumentException("Encoding cannot be null or empty"); - MessageDigest md = getMessageDigestInstance(); + MessageDigest md = getSha256MessageDigestInstance(); try { byte[] bytes = string.getBytes(encoding); @@ -80,7 +81,7 @@ public static byte[] getDigest(String string, String encoding) { * * @return A MessageDigest */ - public static MessageDigest getMessageDigestInstance() { + public static MessageDigest getSha256MessageDigestInstance() { try { return MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException e) { @@ -89,40 +90,36 @@ public static MessageDigest getMessageDigestInstance() { } /** - * Create a final key from the parameters passed + * Gets a SHA-512 message digest instance + * + * @return A MessageDigest */ - public static byte[] getFinalKeyDigest(byte[] key, byte[] masterSeed, byte[] transformSeed, long transformRounds) { - - AESEngine engine = new AESEngine(); - engine.init(true, new KeyParameter(transformSeed)); - - // copy input key - byte[] transformedKey = new byte[key.length]; - System.arraycopy(key, 0, transformedKey, 0, transformedKey.length); - - // transform rounds times - for (long rounds = 0; rounds < transformRounds; rounds++) { - engine.processBlock(transformedKey, 0, transformedKey, 0); - engine.processBlock(transformedKey, 16, transformedKey, 16); + public static MessageDigest getSha512MessageDigestInstance() { + try { + return MessageDigest.getInstance("SHA-512"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-512 is not supported"); } - - MessageDigest md = getMessageDigestInstance(); - byte[] transformedKeyDigest = md.digest(transformedKey); - - md.update(masterSeed); - return md.digest(transformedKeyDigest); } /** * Create a decrypted input stream from an encrypted one */ - public static InputStream getDecryptedInputStream (InputStream encryptedInputStream, byte[] keyData, byte[] ivData) { + public static InputStream getDecryptedInputStream(InputStream encryptedInputStream, PaddedBufferedBlockCipher pbbc, byte[] keyData, byte[] ivData) { final ParametersWithIV keyAndIV = new ParametersWithIV(new KeyParameter(keyData), ivData); - PaddedBufferedBlockCipher pbbc = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); pbbc.init(false, keyAndIV); return new CipherInputStream(encryptedInputStream, pbbc); } + /** + * Create a decrypted input stream from an encrypted one + */ + public static InputStream getDecryptedInputStream(InputStream encryptedInputStream, StreamCipher cipher, byte[] keyData, byte[] ivData) throws IOException { + final ParametersWithIV keyAndIV = new ParametersWithIV(new KeyParameter(keyData), ivData); + cipher.init(false, keyAndIV); + return new CipherInputStream(encryptedInputStream, cipher); + } + /** * Create an encrypted output stream from an unencrypted output stream */ diff --git a/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java b/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java new file mode 100644 index 00000000..3f8ed67e --- /dev/null +++ b/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java @@ -0,0 +1,89 @@ +package org.linguafranca.pwdb.security; + +import org.jetbrains.annotations.Nullable; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Implementation of a storage for KDBX Header field parameters + * + * @author jo + */ + +@SuppressWarnings("WeakerAccess") +public class VariantDictionary { + + private short version; + private Map entries = new HashMap<>(); + + @SuppressWarnings("unused") + public static class Types { + public static final byte UINT32 = 0x4; + public static final byte UINT64 = 0x5; + public static final byte BOOL = 0x8; + public static final byte INT32 = 0xC; + public static final byte INT64 = 0xD; + public static final byte STRING = 0x18; // UTF-8, without BOM, without null terminator + public static final byte ARRRAY = 0x42; + } + + @SuppressWarnings("WeakerAccess") + public static class Entry { + private byte type; + private byte [] value; + + public Entry(byte type, byte [] value) { + this.type = type; + this.value = value; + } + + public byte getType() { + return type; + } + + public UUID asUuid() { + if (value.length == 16) { + ByteBuffer b = ByteBuffer.wrap(value); + return new UUID(b.getLong(), b.getLong(8)); + } + throw new IllegalStateException("Cannot convert value to UUID"); + } + + public long asLong() { + if (value.length != 8){ + throw new IllegalStateException("Cannot convert value to long"); + } + return ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).getLong(); + } + + public int asInteger() { + if (value.length != 4){ + throw new IllegalStateException("Cannot convert value to int"); + } + return ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).getInt(); + } + + public byte [] asByteArray() { + return value; + } + } + + public VariantDictionary(short version) { + this.version = version; + } + public short getVersion() { + return version; + } + + public @Nullable Entry get(String key) { + return entries.get(key); + } + + public void put(String key, byte type, byte [] value) { + entries.put(key, new Entry(type, value)); + } +} diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java index 1ea94d15..5f0b7a68 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java @@ -16,10 +16,9 @@ package org.linguafranca.pwdb.kdbx.dom; -import org.linguafranca.pwdb.Entry; import org.linguafranca.pwdb.base.AbstractDatabase; import org.linguafranca.pwdb.kdbx.Helpers; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxStreamFormat; +import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import org.linguafranca.pwdb.kdbx.StreamFormat; import org.linguafranca.pwdb.Credentials; import org.w3c.dom.Document; diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java index d1cb7809..b2fa7993 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java @@ -16,7 +16,7 @@ package org.linguafranca.pwdb.kdbx.dom; -import org.linguafranca.pwdb.kdbx.stream_3_1.Salsa20StreamEncryptor; + import org.linguafranca.pwdb.kdbx.SerializableDatabase; import org.linguafranca.pwdb.kdbx.StreamEncryptor; import org.apache.commons.codec.binary.Base64; @@ -74,7 +74,6 @@ public static DomSerializableDatabase createEmptyDatabase() throws IOException { } catch (XPathExpressionException e) { throw new IllegalStateException(e); } - result.setEncryption(new Salsa20StreamEncryptor(SecureRandom.getSeed(32))); return result; } diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java index a4de349e..c7612b1b 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java @@ -20,7 +20,7 @@ import org.junit.Test; import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.kdbx.KdbxCreds; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxStreamFormat; +import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import org.linguafranca.pwdb.kdbx.StreamFormat; import org.linguafranca.pwdb.Credentials; diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/VisitorTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/VisitorTest.java index 43675e26..4c908349 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/VisitorTest.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/VisitorTest.java @@ -22,7 +22,7 @@ import org.linguafranca.pwdb.Entry; import org.linguafranca.pwdb.Visitor; import org.linguafranca.pwdb.kdbx.KdbxCreds; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxStreamFormat; +import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import java.io.InputStream; import java.util.ArrayList; diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java index 7950442e..2ac0d480 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java @@ -16,8 +16,6 @@ package org.linguafranca.pwdb.kdbx; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxSerializer; -import org.linguafranca.pwdb.kdbx.stream_3_1.Salsa20StreamEncryptor; import org.linguafranca.pwdb.Credentials; import org.xml.sax.*; @@ -37,10 +35,10 @@ public class SaxParse { public void exampleSaxparsing() throws IOException, SAXException, ParserConfigurationException { InputStream encryptedInputStream = getClass().getClassLoader().getResourceAsStream("test123.kdbx"); Credentials credentials = new KdbxCreds("123".getBytes()); - org.linguafranca.pwdb.kdbx.stream_3_1.KdbxHeader kdbxHeader = new org.linguafranca.pwdb.kdbx.stream_3_1.KdbxHeader(); + KdbxHeader kdbxHeader = new KdbxHeader(); try (InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, kdbxHeader, encryptedInputStream)) { // use this to decrypt the encrypted fields - final org.linguafranca.pwdb.kdbx.stream_3_1.Salsa20StreamEncryptor memoryProtection = new Salsa20StreamEncryptor(kdbxHeader.getProtectedStreamKey()); + final StreamEncryptor memoryProtection = new StreamEncryptor.Salsa20(kdbxHeader.getInnerRandomStreamKey()); SAXParserFactory spfactory = SAXParserFactory.newInstance(); SAXParser saxParser = spfactory.newSAXParser(); XMLReader xmlReader = saxParser.getXMLReader(); diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java index 884b8b5a..85c785ab 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java @@ -18,7 +18,7 @@ import org.jetbrains.annotations.NotNull; import org.linguafranca.pwdb.base.AbstractDatabase; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxStreamFormat; +import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import org.linguafranca.pwdb.kdbx.StreamFormat; import org.linguafranca.pwdb.kdbx.jaxb.binding.KeePassFile; import org.linguafranca.pwdb.kdbx.jaxb.binding.ObjectFactory; diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java index b9231ccd..94982d88 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java @@ -40,7 +40,7 @@ class Password implements KdbCredentials { private final byte [] key; public Password(byte[] password) { - MessageDigest md = Encryption.getMessageDigestInstance(); + MessageDigest md = Encryption.getSha256MessageDigestInstance(); this.key = md.digest(password); } @@ -58,7 +58,7 @@ class KeyFile implements KdbCredentials { private final byte[] key; public KeyFile(byte[] password, InputStream inputStream) { - MessageDigest md = Encryption.getMessageDigestInstance(); + MessageDigest md = Encryption.getSha256MessageDigestInstance(); byte[] pwKey = md.digest(password); md.update(pwKey); diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbHeader.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbHeader.java index 728605b3..4efc1307 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbHeader.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbHeader.java @@ -16,6 +16,7 @@ package org.linguafranca.pwdb.kdb; +import org.linguafranca.pwdb.security.Aes; import org.linguafranca.pwdb.security.Encryption; import javax.crypto.Cipher; @@ -61,8 +62,8 @@ public InputStream createDecryptedInputStream(byte[] key, InputStream inputStrea throw new IllegalStateException("StreamEncryptor algorithm is not supported"); } - byte[] finalKeyDigest = Encryption.getFinalKeyDigest(key, masterSeed, transformSeed, transformRounds); - return Encryption.getDecryptedInputStream(inputStream, finalKeyDigest, encryptionIv); + byte[] finalKeyDigest = Aes.getFinalKeyDigest(key, masterSeed, transformSeed, transformRounds); + return Encryption.getDecryptedInputStream(inputStream, Aes.getCipher(), finalKeyDigest, encryptionIv); } public int getFlags() { diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java index 45a211a2..62b0891d 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java @@ -17,7 +17,6 @@ package org.linguafranca.pwdb.kdb; import com.google.common.io.LittleEndianDataInputStream; -import org.linguafranca.pwdb.base.AbstractGroup; import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.Group; import org.linguafranca.pwdb.security.Encryption; @@ -79,7 +78,7 @@ public static KdbDatabase createKdbDatabase(Credentials credentials, KdbHeader k InputStream decryptedInputStream = kdbHeader.createDecryptedInputStream(credentials.getKey(), inputStream); // Wrap the decrypted stream in a digest stream - MessageDigest digest = Encryption.getMessageDigestInstance(); + MessageDigest digest = Encryption.getSha256MessageDigestInstance(); DigestInputStream digestInputStream = new DigestInputStream(decryptedInputStream, digest); // Start the dataInput at wherever we have got to in the stream diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java new file mode 100644 index 00000000..9594cd85 --- /dev/null +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java @@ -0,0 +1,98 @@ +package org.linguafranca.pwdb.hashedblock; + +import com.google.common.io.LittleEndianDataInputStream; +import org.jetbrains.annotations.NotNull; + +import java.io.*; + +/** + * Takes an underlying stream formatted as HMAC Hashed Blocks and provides + * the content of the blocks as a stream. + * + * @author jo + */ +public class HmacBlockInputStream extends FilterInputStream { + + private final boolean littleEndian; + private volatile ByteArrayInputStream bufferStream; + private final DataInput input; + private boolean finished; + private int blockCount = 0; + + public HmacBlockInputStream(InputStream inputStream) throws IOException { + this(inputStream, false); + } + + public HmacBlockInputStream(InputStream inputStream, boolean littleEndian) throws IOException { + super(inputStream); + this.littleEndian = littleEndian; + if (littleEndian) { + input = new LittleEndianDataInputStream(in); + } else { + input = new DataInputStream(in); + } + getBlock(); + } + + private void getBlock() throws IOException { + // get the HMAC + byte [] hmacSha256 = new byte [32]; + input.readFully(hmacSha256); + + // get the block size + int blockSize = input.readInt(); + if (blockSize == 0) { + finished = true; + } + + // read the new block + byte [] buffer = new byte [blockSize]; + input.readFully(buffer); + + verifyHmac(buffer); + + // create a new internal stream for the block + bufferStream = new ByteArrayInputStream(buffer); + blockCount ++; + } + + private void verifyHmac(byte[] buffer) { + // TODO actually verify the HMAC + } + + @Override + public int read(@NotNull byte[] b) throws IOException { + return read(b , 0 , b.length); + } + + @Override + public int read(@NotNull byte[] b, int off, int len) throws IOException { + if (finished) { + return -1; + } + int bytesRead = bufferStream.read(b, off, len); + if (bufferStream.available() == 0) { + getBlock(); + } + return bytesRead; + } + + @Override + public int read() throws IOException { + if (finished) { + return -1; + } + int result = bufferStream.read(); + if (result == -1) { + getBlock(); + if (finished) { + return -1; + } + result = bufferStream.read(); + if (result == -1) { + throw new IOException("Can't replenish buffer"); + } + } + return result; + } +} diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxCredentials.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxCredentials.java index 0163fc0f..3863dbee 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxCredentials.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxCredentials.java @@ -46,7 +46,7 @@ class KeyFile implements KdbxCredentials { * @param inputStream inputstream of the keyfile */ public KeyFile(@NotNull byte[] password, @NotNull InputStream inputStream) { - MessageDigest md = Encryption.getMessageDigestInstance(); + MessageDigest md = Encryption.getSha256MessageDigestInstance(); byte[] pwKey = md.digest(password); md.update(pwKey); @@ -62,7 +62,7 @@ public KeyFile(@NotNull byte[] password, @NotNull InputStream inputStream) { * @param inputStream inputstream of the keyfile */ public KeyFile(@NotNull InputStream inputStream) { - MessageDigest md = Encryption.getMessageDigestInstance(); + MessageDigest md = Encryption.getSha256MessageDigestInstance(); byte[] keyFileData = KdbxKeyFile.load(inputStream); if (keyFileData == null) { @@ -85,7 +85,7 @@ class Password implements KdbxCredentials { private final byte[] key; public Password(@NotNull byte[] password) { - MessageDigest md = Encryption.getMessageDigestInstance(); + MessageDigest md = Encryption.getSha256MessageDigestInstance(); byte[] digest = md.digest(password); key = md.digest(digest); } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxCreds.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxCreds.java index edf3e17f..dff3d242 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxCreds.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxCreds.java @@ -38,7 +38,7 @@ public class KdbxCreds implements Credentials { * @param inputStream inputstream of the keyfile */ public KdbxCreds(@NotNull byte[] password, @NotNull InputStream inputStream) { - MessageDigest md = Encryption.getMessageDigestInstance(); + MessageDigest md = Encryption.getSha256MessageDigestInstance(); byte[] pwKey = md.digest(password); md.update(pwKey); @@ -54,7 +54,7 @@ public KdbxCreds(@NotNull byte[] password, @NotNull InputStream inputStream) { * @param inputStream inputstream of the keyfile */ public KdbxCreds(@NotNull InputStream inputStream) { - MessageDigest md = Encryption.getMessageDigestInstance(); + MessageDigest md = Encryption.getSha256MessageDigestInstance(); byte[] keyFileData = KdbxKeyFile.load(inputStream); if (keyFileData == null) { throw new IllegalStateException("Could not read key file"); @@ -64,7 +64,7 @@ public KdbxCreds(@NotNull InputStream inputStream) { public KdbxCreds(@NotNull byte[] password) { - MessageDigest md = Encryption.getMessageDigestInstance(); + MessageDigest md = Encryption.getSha256MessageDigestInstance(); byte[] digest = md.digest(password); key = md.digest(digest); } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxHeader.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java similarity index 61% rename from kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxHeader.java rename to kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java index bbfef5d1..28c59f32 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxHeader.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java @@ -14,16 +14,18 @@ * limitations under the License. */ -package org.linguafranca.pwdb.kdbx.stream_3_1; +package org.linguafranca.pwdb.kdbx; -import org.linguafranca.pwdb.kdbx.StreamFormat; -import org.linguafranca.pwdb.security.Encryption; +import org.linguafranca.pwdb.security.*; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.UUID; /** @@ -56,26 +58,43 @@ public enum CompressionFlags { */ @SuppressWarnings("WeakerAccess, unused") public enum ProtectedStreamAlgorithm { - NONE, ARC_FOUR, SALSA_20 + NONE, ARC_FOUR, SALSA_20, CHA_CHA_20 } /** - * This UUID denotes that AES Cipher is in use. No other values are known. + * UUIDs of Ciphers for encryption */ public static final UUID AES_CIPHER = UUID.fromString("31C1F2E6-BF71-4350-BE58-05216AFC5AFF"); + public static final UUID CHACHA_CIPHER = UUID.fromString("d6038a2b-8b6f-4cb5-a524-339a31dbb59a"); + + private List allowableVersions = new ArrayList<>(Arrays.asList(3,4)); + + + /* version of the file */ + private int version; + + protected UUID cipherUuid; + private byte [] masterSeed; + private byte[] encryptionIv; - /* the cipher in use */ - private UUID cipherUuid; /* whether the data is compressed */ private CompressionFlags compressionFlags; - private byte [] masterSeed; + + /* V3 fields */ private byte[] transformSeed; private long transformRounds; - private byte[] encryptionIv; - private byte[] protectedStreamKey; + + /* header (V3) inner header (v4) */ + private byte[] innerRandomStreamKey; private ProtectedStreamAlgorithm protectedStreamAlgorithm; - /* these bytes appear in cipher text immediately following the header */ + + /* these bytes appear in cipher text immediately following the header (V3) */ private byte[] streamStartBytes; + + /* dictionaries in V4 */ + private VariantDictionary kdfparameters; + private VariantDictionary customData; + /* not transmitted as part of the header, used in the XML payload, so calculated * on transmission or receipt */ private byte[] headerHash; @@ -91,9 +110,10 @@ public KdbxHeader() { transformSeed = random.generateSeed(32); transformRounds = 6000; encryptionIv = random.generateSeed(16); - protectedStreamKey = random.generateSeed(32); + innerRandomStreamKey = random.generateSeed(32); streamStartBytes = new byte[32]; protectedStreamAlgorithm = ProtectedStreamAlgorithm.SALSA_20; + version = 3; } /** @@ -106,8 +126,27 @@ public KdbxHeader() { * @throws IOException if something bad happens */ public InputStream createDecryptedStream(byte[] digest, InputStream inputStream) throws IOException { - byte[] finalKeyDigest = Encryption.getFinalKeyDigest(digest, getMasterSeed(), getTransformSeed(), getTransformRounds()); - return Encryption.getDecryptedInputStream(inputStream, finalKeyDigest, getEncryptionIv()); + byte[] finalKeyDigest; + + UUID kdf = null; + if (kdfparameters != null) { + kdf = kdfparameters.get("$UUID").asUuid(); + } + // v3 doesn't have a kdf therefore AES + if (kdf == null || Aes.KDF.equals(kdf)){ + finalKeyDigest = Aes.getFinalKeyDigest(digest, getMasterSeed(), getTransformSeed(), getTransformRounds()); + } else if (Argon.argon2_kdf.equals(kdf)) { + finalKeyDigest = Argon.getArgonFinalKeyDigest(digest, getMasterSeed(), kdfparameters); + } else { + throw new UnsupportedOperationException("Unknown transform KDF " + kdf); + } + + if (AES_CIPHER.equals(cipherUuid)) { + return Encryption.getDecryptedInputStream(inputStream, Aes.getCipher(), finalKeyDigest, getEncryptionIv()); + } else if (CHACHA_CIPHER.equals(cipherUuid)) { + return Encryption.getDecryptedInputStream(inputStream, ChaCha.getCipher(), finalKeyDigest, getEncryptionIv()); + } + throw new UnsupportedOperationException("Unknown encryption cipher " + cipherUuid); } /** @@ -119,10 +158,24 @@ public InputStream createDecryptedStream(byte[] digest, InputStream inputStream) * @throws IOException if something bad happens */ public OutputStream createEncryptedStream(byte[] digest, OutputStream outputStream) throws IOException { - byte[] finalKeyDigest = Encryption.getFinalKeyDigest(digest, getMasterSeed(), getTransformSeed(), getTransformRounds()); + byte[] finalKeyDigest = Aes.getFinalKeyDigest(digest, getMasterSeed(), getTransformSeed(), getTransformRounds()); return Encryption.getEncryptedOutputStream(outputStream, finalKeyDigest, getEncryptionIv()); } + public byte[] getTransformSeed() { + if (version < 4) { + return transformSeed; + } + return kdfparameters.get(Aes.KdfKeys.ParamSeed).asByteArray(); + } + + public long getTransformRounds() { + if (version < 4) { + return transformRounds; + } + return kdfparameters.get(Aes.KdfKeys.ParamRounds).asLong(); + } + public UUID getCipherUuid() { return cipherUuid; } @@ -135,20 +188,12 @@ public byte[] getMasterSeed() { return masterSeed; } - public byte[] getTransformSeed() { - return transformSeed; - } - - public long getTransformRounds() { - return transformRounds; - } - public byte[] getEncryptionIv() { return encryptionIv; } - public byte[] getProtectedStreamKey() { - return protectedStreamKey; + public byte[] getInnerRandomStreamKey() { + return innerRandomStreamKey; } public byte[] getStreamStartBytes() { @@ -163,13 +208,18 @@ public byte[] getHeaderHash() { return headerHash; } - public void setCipherUuid(byte[] uuid) { - ByteBuffer b = ByteBuffer.wrap(uuid); - UUID incoming = new UUID(b.getLong(), b.getLong(8)); - if (!incoming.equals(AES_CIPHER)) { - throw new IllegalStateException("Unknown Cipher UUID " + incoming.toString()); + public int getVersion() { + return version; + } + + public StreamEncryptor getStreamEncryptor() { + switch (getProtectedStreamAlgorithm()) { + case NONE: {throw new IllegalStateException("Inner stream encoding of NONE");} + case ARC_FOUR: {throw new UnsupportedOperationException("Arc Four inner stream not supported");} + case SALSA_20: {return new StreamEncryptor.Salsa20(this.innerRandomStreamKey);} + case CHA_CHA_20: {return new StreamEncryptor.ChaCha20(this.innerRandomStreamKey);} } - this.cipherUuid = incoming; + throw new IllegalStateException("Inner stream encoding unsupported"); } public void setCompressionFlags(int flags) { @@ -192,8 +242,8 @@ public void setEncryptionIv(byte[] encryptionIv) { this.encryptionIv = encryptionIv; } - public void setProtectedStreamKey(byte[] protectedStreamKey) { - this.protectedStreamKey = protectedStreamKey; + public void setInnerRandomStreamKey(byte[] key) { + this.innerRandomStreamKey = key; } public void setStreamStartBytes(byte[] streamStartBytes) { @@ -207,4 +257,33 @@ public void setInnerRandomStreamId(int innerRandomStreamId) { public void setHeaderHash(byte[] headerHash) { this.headerHash = headerHash; } + + public void setCipherUuid(byte[] uuid) { + ByteBuffer b = ByteBuffer.wrap(uuid); + UUID incoming = new UUID(b.getLong(), b.getLong(8)); + if (!incoming.equals(AES_CIPHER) && !incoming.equals(CHACHA_CIPHER)) { + throw new IllegalStateException("Unknown Cipher UUID " + incoming.toString()); + } + this.cipherUuid = incoming; + } + + public void setVersion(int version) { + if (!allowableVersions.contains(version)) { + throw new IllegalStateException("File version must be in " + allowableVersions.toString()); + } + this.version = version; + } + + public void setKdfparameters(VariantDictionary kdfparameters) { + this.kdfparameters = kdfparameters; + } + + public void setCustomData(VariantDictionary customData) { + this.customData = customData; + } + + public void addBinary(byte[] bytes) { + + } + } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java similarity index 56% rename from kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.java rename to kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java index c2477012..22d96b79 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java @@ -14,17 +14,20 @@ * limitations under the License. */ -package org.linguafranca.pwdb.kdbx.stream_3_1; +package org.linguafranca.pwdb.kdbx; import com.google.common.io.LittleEndianDataInputStream; import com.google.common.io.LittleEndianDataOutputStream; +import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.hashedblock.HashedBlockInputStream; import org.linguafranca.pwdb.hashedblock.HashedBlockOutputStream; -import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.hashedblock.HmacBlockInputStream; import org.linguafranca.pwdb.security.Encryption; +import org.linguafranca.pwdb.security.VariantDictionary; import java.io.*; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.security.DigestInputStream; import java.security.DigestOutputStream; import java.security.MessageDigest; @@ -33,7 +36,7 @@ import java.util.zip.GZIPOutputStream; /** - * This class provides static methods for the encryption and decryption of Keepass KDBX files. + * This class provides static methods for the encryption and decryption of Keepass KDBX V4 files. *

* A KDBX file is little-endian and consists of the following: *

    @@ -74,29 +77,45 @@ private KdbxSerializer() {} /** * Provides the payload of a KDBX file as an unencrypted {@link InputStream}. * @param credentials credentials for decryption of the stream - * @param kdbxHeader a header instance to be populated with values from the stream + * @param kdbxHeader a KdbxHeader for the encryption parameters and so on * @param inputStream a KDBX formatted input stream * @return an unencrypted input stream, to be read and closed by the caller * @throws IOException on error */ public static InputStream createUnencryptedInputStream(Credentials credentials, KdbxHeader kdbxHeader, InputStream inputStream) throws IOException { - readKdbxHeader(kdbxHeader, inputStream); + readOuterHeader(inputStream, kdbxHeader); + + InputStream plainTextStream; + + if (kdbxHeader.getVersion() >= 4) { + + HmacBlockInputStream hmacBlockInputStream = new HmacBlockInputStream(inputStream, true); - InputStream decryptedInputStream = kdbxHeader.createDecryptedStream(credentials.getKey(), inputStream); + plainTextStream = kdbxHeader.createDecryptedStream(credentials.getKey(), hmacBlockInputStream); + } else { - checkStartBytes(kdbxHeader, decryptedInputStream); + InputStream decryptedInputStream = kdbxHeader.createDecryptedStream(credentials.getKey(), inputStream); - HashedBlockInputStream blockInputStream = new HashedBlockInputStream(decryptedInputStream, true); + checkStartBytes(kdbxHeader, decryptedInputStream); + + plainTextStream = new HashedBlockInputStream(decryptedInputStream, true); + } + + if (kdbxHeader.getCompressionFlags().equals(KdbxHeader.CompressionFlags.GZIP)) { + plainTextStream = new GZIPInputStream(plainTextStream); + } - if (kdbxHeader.getCompressionFlags().equals(KdbxHeader.CompressionFlags.NONE)) { - return blockInputStream; + if (kdbxHeader.getVersion() >= 4) { + readInnerHeader(kdbxHeader, plainTextStream); } - return new GZIPInputStream(blockInputStream); + + return plainTextStream; } /** * Provides an {@link OutputStream} to be encoded and encrypted in KDBX format + * // TODO only writes in V3 format * @param credentials credentials for encryption of the stream * @param kdbxHeader a KDBX header to control the formatting and encryption operation * @param outputStream output stream to contain the KDBX formatted output @@ -139,6 +158,7 @@ private static void writeStartBytes(KdbxHeader kdbxHeader, OutputStream encrypte private static final int SIG2 = 0xB54BFB67; private static final int FILE_VERSION_CRITICAL_MASK = 0xFFFF0000; private static final int FILE_VERSION_32 = 0x00030001; + private static final int FILE_VERSION_4 = 0x00040000; private static class HeaderType { static final byte END = 0; @@ -149,9 +169,11 @@ private static class HeaderType { static final byte TRANSFORM_SEED = 5; static final byte TRANSFORM_ROUNDS = 6; static final byte ENCRYPTION_IV = 7; - static final byte PROTECTED_STREAM_KEY = 8; + static final byte INNER_RANDOM_STREAM_KEY = 8; static final byte STREAM_START_BYTES = 9; static final byte INNER_RANDOM_STREAM_ID = 10; + static final byte KDF_PARAMETERS = 11; + static final byte CUSTOM_DATA = 12; } /** @@ -168,93 +190,212 @@ private static boolean verifyMagicNumber(LittleEndianDataInputStream ledis) thro } /** - * Read 4 bytes and make sure they conform to expectations of file version - * @param ledis an input stream - * @return true if it looks like we understand this file version - * @throws IOException on error - */ - private static boolean verifyFileVersion(LittleEndianDataInputStream ledis) throws IOException { - return ((ledis.readInt() & FILE_VERSION_CRITICAL_MASK) <= (FILE_VERSION_32 & FILE_VERSION_CRITICAL_MASK)); - } - - /** - * Populate a KdbxHeader from the input stream supplied - * @param kdbxHeader a header to be populated + * Create and populate a KdbxHeader from the input stream supplied * @param inputStream an input stream * @return the populated KdbxHeader * @throws IOException on error */ - public static KdbxHeader readKdbxHeader(KdbxHeader kdbxHeader, InputStream inputStream) throws IOException { + public static KdbxHeader readOuterHeader(InputStream inputStream, KdbxHeader kdbxHeader) throws IOException { - MessageDigest digest = Encryption.getMessageDigestInstance(); + // header is digested to verify correctness + MessageDigest digest = Encryption.getSha256MessageDigestInstance(); // we do not close this stream, otherwise we lose our place in the underlying stream - DigestInputStream digestInputStream = new DigestInputStream(inputStream, digest); + DigestInputStream shaDigestInputStream = new DigestInputStream(inputStream, digest); // we do not close this stream, otherwise we lose our place in the underlying stream - LittleEndianDataInputStream ledis = new LittleEndianDataInputStream(digestInputStream); - + LittleEndianDataInputStream ledis = new LittleEndianDataInputStream(shaDigestInputStream); + // file starts with magic number if (!verifyMagicNumber(ledis)) { throw new IllegalStateException("Magic number did not match"); } + // followed by a file vesion number + int fullVersion = ledis.readInt(); + kdbxHeader.setVersion(fullVersion >> 16); + + // read header fields + getOuterHeaderFields(kdbxHeader, digest, ledis); - if (!verifyFileVersion(ledis)) { - throw new IllegalStateException("File version did not match"); + if (kdbxHeader.getVersion() > 3) { + // v4 contains header hashes + verifyOuterHeader(kdbxHeader, ledis); } - + + return kdbxHeader; + } + + /** + * V4 header is followed by an SHA256 and then contains an HMACSHA256 after that. + * @param kdbxHeader the header containing the relevant parameters + * @param input an input source + * @throws IOException on error + */ + private static void verifyOuterHeader(KdbxHeader kdbxHeader, DataInput input) throws IOException { + byte [] sha256 = getBytes(32, input); + if (!Arrays.equals(kdbxHeader.getHeaderHash(), sha256)) { + throw new IllegalStateException("Header hash does not match"); + } + byte [] hmacSha256 = getBytes(32, input); + // TODO verify HMAC + } + + private static void getOuterHeaderFields(KdbxHeader kdbxHeader, MessageDigest digest, DataInput input) throws IOException { byte headerType; - while ((headerType = ledis.readByte()) != HeaderType.END) { + do { + headerType = input.readByte(); + int length = (kdbxHeader.getVersion() == 3 ? input.readShort() : input.readInt()); + switch (headerType) { + case HeaderType.END: { + getBytes(length, input); + break; + } + case HeaderType.COMMENT: - getByteArray(ledis); + getBytes(length, input); break; case HeaderType.CIPHER_ID: - kdbxHeader.setCipherUuid(getByteArray(ledis)); + kdbxHeader.setCipherUuid(getBytes(length, input)); break; case HeaderType.COMPRESSION_FLAGS: - kdbxHeader.setCompressionFlags(getInt(ledis)); + kdbxHeader.setCompressionFlags(getInt(length, input)); break; case HeaderType.MASTER_SEED: - kdbxHeader.setMasterSeed(getByteArray(ledis)); + kdbxHeader.setMasterSeed(getBytes(length, input)); break; case HeaderType.TRANSFORM_SEED: - kdbxHeader.setTransformSeed(getByteArray(ledis)); + kdbxHeader.setTransformSeed(getBytes(length, input)); break; case HeaderType.TRANSFORM_ROUNDS: - kdbxHeader.setTransformRounds(getLong(ledis)); + kdbxHeader.setTransformRounds(getLong(length, input)); break; case HeaderType.ENCRYPTION_IV: - kdbxHeader.setEncryptionIv(getByteArray(ledis)); + kdbxHeader.setEncryptionIv(getBytes(length, input)); break; - case HeaderType.PROTECTED_STREAM_KEY: - kdbxHeader.setProtectedStreamKey(getByteArray(ledis)); + case HeaderType.INNER_RANDOM_STREAM_KEY: + kdbxHeader.setInnerRandomStreamKey(getBytes(length, input)); break; case HeaderType.STREAM_START_BYTES: - kdbxHeader.setStreamStartBytes(getByteArray(ledis)); + kdbxHeader.setStreamStartBytes(getBytes(length, input)); break; case HeaderType.INNER_RANDOM_STREAM_ID: - kdbxHeader.setInnerRandomStreamId(getInt(ledis)); + kdbxHeader.setInnerRandomStreamId(getInt(length, input)); + break; + + case HeaderType.KDF_PARAMETERS: + kdbxHeader.setKdfparameters(makeVariantDictionary(length, input)); + break; + + case HeaderType.CUSTOM_DATA: + kdbxHeader.setCustomData(makeVariantDictionary(length, input)); break; default: throw new IllegalStateException("Unknown File Header"); } + } while (headerType != HeaderType.END); + + kdbxHeader.setHeaderHash(digest.digest()); + } + + /** + * Type fieds for inner headers + * @see KdbxSerializer#readInnerHeader + */ + private static class InnerHeaderType { + private static final byte END = 0; + private static final byte INNER_RANDOM_STREAM_ID = 1; // Supersedes KdbxHeaderFieldID.InnerRandomStreamID + private static final byte INNER_RANDOM_STREAM_KEY = 2; // Supersedes KdbxHeaderFieldID.InnerRandomStreamKey + private static final byte BINARY = 3; + } + + /** + * From V4 the inner stream encryption parameters are contained in + * a set of headers immediately preceding the XML payload + * @param kdbxHeader the header whose values are to be read + * @param plainTextStream a stream to read them from + * @throws IOException on error + */ + private static void readInnerHeader(KdbxHeader kdbxHeader, InputStream plainTextStream) throws IOException { + DataInput input = new LittleEndianDataInputStream(plainTextStream); + + byte headerType; + do { + headerType = input.readByte(); + int length = input.readInt(); + + switch (headerType) { + case InnerHeaderType.END: { + getBytes(length, input); + break; + } + + case InnerHeaderType.INNER_RANDOM_STREAM_ID: { + kdbxHeader.setInnerRandomStreamId(getInt(length, input)); + break; + } + + case InnerHeaderType.INNER_RANDOM_STREAM_KEY: { + kdbxHeader.setInnerRandomStreamKey(getBytes(length, input)); + break; + } + + case InnerHeaderType.BINARY: { + kdbxHeader.addBinary(getBytes(length, input)); + break; + } + + default: throw new IllegalStateException("Invalid inner header field"); + } + } while (headerType != HeaderType.END); + } + + /** + * Read a VariantDictionary from the supplied input + * @param input source of data + * @return a VariantDictionary + * @throws IOException on error + */ + private static VariantDictionary makeVariantDictionary(int length, DataInput input) throws IOException { + // read the buffer containing the dictionary, which starts with a 4 byte length + ByteBuffer buf = ByteBuffer.wrap(getBytes(length, input)); + buf.order(ByteOrder.LITTLE_ENDIAN); + // version number must be 0x01?? + + VariantDictionary vd = new VariantDictionary(buf.getShort()); + if ((vd.getVersion() & 0xFF00) != 0x0100) { + throw new IllegalStateException("Variant dictionary must have version 0x0100"); } - // consume length etc. following END flag - getByteArray(ledis); + // sequence of entries followed by a byte 0 + byte type = buf.get(); + while (type != 0) { + // get key + int keylength = buf.getInt(); + byte [] key = new byte[keylength]; + buf.get(key); - kdbxHeader.setHeaderHash(digest.digest()); - return kdbxHeader; + // get value + int valueLength = buf.getInt(); + byte [] value = new byte[valueLength]; + buf.get(value); + + // add entry + vd.put(new String(key), type, value); + + type = buf.get(); + } + return vd; } + /** * Write a KdbxHeader to the output stream supplied. The header is updated with the * message digest of the written stream. @@ -263,7 +404,7 @@ public static KdbxHeader readKdbxHeader(KdbxHeader kdbxHeader, InputStream input * @throws IOException on error */ public static void writeKdbxHeader(KdbxHeader kdbxHeader, OutputStream outputStream) throws IOException { - MessageDigest messageDigest = Encryption.getMessageDigestInstance(); + MessageDigest messageDigest = Encryption.getSha256MessageDigestInstance(); DigestOutputStream digestOutputStream = new DigestOutputStream(outputStream, messageDigest); LittleEndianDataOutputStream ledos = new LittleEndianDataOutputStream(digestOutputStream); @@ -301,9 +442,9 @@ public static void writeKdbxHeader(KdbxHeader kdbxHeader, OutputStream outputStr ledos.writeShort(kdbxHeader.getEncryptionIv().length); ledos.write(kdbxHeader.getEncryptionIv()); - ledos.writeByte(HeaderType.PROTECTED_STREAM_KEY); - ledos.writeShort(kdbxHeader.getProtectedStreamKey().length); - ledos.write(kdbxHeader.getProtectedStreamKey()); + ledos.writeByte(HeaderType.INNER_RANDOM_STREAM_KEY); + ledos.writeShort(kdbxHeader.getInnerRandomStreamKey().length); + ledos.write(kdbxHeader.getInnerRandomStreamKey()); ledos.writeByte(HeaderType.STREAM_START_BYTES); ledos.writeShort(kdbxHeader.getStreamStartBytes().length); @@ -321,26 +462,23 @@ public static void writeKdbxHeader(KdbxHeader kdbxHeader, OutputStream outputStr } - private static int getInt(LittleEndianDataInputStream ledis) throws IOException { - short fieldLength = ledis.readShort(); - if (fieldLength != 4) { - throw new IllegalStateException("Int required but length was " + fieldLength); + private static int getInt(int length, DataInput input) throws IOException { + if (length != 4) { + throw new IllegalStateException("Int required but length was " + length); } - return ledis.readInt(); + return input.readInt(); } - private static long getLong(LittleEndianDataInputStream ledis) throws IOException { - short fieldLength = ledis.readShort(); - if (fieldLength != 8) { - throw new IllegalStateException("Long required but length was " + fieldLength); + private static long getLong(int length, DataInput input) throws IOException { + if (length != 8) { + throw new IllegalStateException("Long required but length was " + length); } - return ledis.readLong(); + return input.readLong(); } - private static byte [] getByteArray(LittleEndianDataInputStream ledis) throws IOException { - short fieldLength = ledis.readShort(); - byte [] value = new byte[fieldLength]; - ledis.readFully(value); - return value; + private static byte[] getBytes(int numBytes, DataInput input) throws IOException { + byte [] result = new byte[numBytes]; + input.readFully(result); + return result; } } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxStreamFormat.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java similarity index 83% rename from kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxStreamFormat.java rename to kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java index 62aa4398..d0dde97a 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxStreamFormat.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java @@ -14,10 +14,8 @@ * limitations under the License. */ -package org.linguafranca.pwdb.kdbx.stream_3_1; +package org.linguafranca.pwdb.kdbx; -import org.linguafranca.pwdb.kdbx.SerializableDatabase; -import org.linguafranca.pwdb.kdbx.StreamFormat; import org.linguafranca.pwdb.Credentials; import java.io.IOException; @@ -35,7 +33,7 @@ public class KdbxStreamFormat implements StreamFormat { public void load(SerializableDatabase serializableDatabase, Credentials credentials, InputStream encryptedInputStream) throws IOException { KdbxHeader kdbxHeader = new KdbxHeader(); InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, kdbxHeader, encryptedInputStream); - serializableDatabase.setEncryption(new Salsa20StreamEncryptor(kdbxHeader.getProtectedStreamKey())); + serializableDatabase.setEncryption(kdbxHeader.getStreamEncryptor()); serializableDatabase.load(decryptedInputStream); decryptedInputStream.close(); } @@ -46,7 +44,7 @@ public void save(SerializableDatabase serializableDatabase, Credentials credenti KdbxHeader kdbxHeader = new KdbxHeader(); OutputStream unencrytedOutputStream = KdbxSerializer.createEncryptedOutputStream(credentials, kdbxHeader, encryptedOutputStream); serializableDatabase.setHeaderHash(kdbxHeader.getHeaderHash()); - serializableDatabase.setEncryption(new Salsa20StreamEncryptor(kdbxHeader.getProtectedStreamKey())); + serializableDatabase.setEncryption(kdbxHeader.getStreamEncryptor()); serializableDatabase.save(unencrytedOutputStream); unencrytedOutputStream.flush(); unencrytedOutputStream.close(); diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamEncryptor.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamEncryptor.java index b624dbf8..15f791c2 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamEncryptor.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamEncryptor.java @@ -16,6 +16,16 @@ package org.linguafranca.pwdb.kdbx; +import org.linguafranca.pwdb.security.Encryption; +import org.spongycastle.crypto.StreamCipher; +import org.spongycastle.crypto.engines.ChaCha7539Engine; +import org.spongycastle.crypto.engines.Salsa20Engine; +import org.spongycastle.crypto.params.KeyParameter; +import org.spongycastle.crypto.params.ParametersWithIV; +import org.spongycastle.util.encoders.Hex; + +import java.security.MessageDigest; + /** * KDBX "protected" fields are stream encrypted. They must be decrypted in * the same order as they were encrypted. @@ -46,4 +56,80 @@ public byte[] encrypt(byte[] decryptedText) { return decryptedText; } } + + class ChaCha20 extends Default { + public ChaCha20(byte[] key) { + super(new ChaCha7539Engine(), key); + + MessageDigest md = Encryption.getSha512MessageDigestInstance(); + md.update(key); + byte [] digest = md.digest(); + + byte [] keyDigest = new byte [32]; + byte [] iv = new byte [12]; + System.arraycopy(digest, 0, keyDigest, 0, keyDigest.length); + System.arraycopy(digest, 32, iv, 0, iv.length); + initialize(keyDigest, iv); + } + } + + class Salsa20 extends Default { + private static final byte[] SALSA20_IV = Hex.decode("E830094B97205D2A".getBytes()); + + public Salsa20(byte[] key) { + super(new Salsa20Engine(), key); + + MessageDigest md = Encryption.getSha256MessageDigestInstance(); + initialize(md.digest(key), SALSA20_IV); + } + } + + class Default implements StreamEncryptor { + + private final StreamCipher cipher; + private final byte[] key; + + + /** + * Initializes an engine + * + * @param key the key to use + * @param iv the iv + */ + public void initialize(byte[] key, byte [] iv) { + KeyParameter keyParameter = new KeyParameter(key); + ParametersWithIV ivParameter = new ParametersWithIV(keyParameter, iv); + + cipher.init(true, ivParameter); + } + + /** + * @param cipher the cipher to use + * @param key the key to use + */ + public Default(StreamCipher cipher, byte[] key) { + this.key = key; + this.cipher = cipher; + } + + @Override + public byte[] getKey() { + return key; + } + + @Override + public byte[] decrypt(byte[] encryptedText) { + byte[] output = new byte[encryptedText.length]; + cipher.processBytes(encryptedText, 0, encryptedText.length, output, 0); + return output; + } + + @Override + public byte[] encrypt(byte[] decryptedText) { + byte[] output = new byte[decryptedText.length]; + cipher.processBytes(decryptedText, 0, decryptedText.length, output, 0); + return output; + } + } + } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/Salsa20StreamEncryptor.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/Salsa20StreamEncryptor.java deleted file mode 100644 index ce48264a..00000000 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/Salsa20StreamEncryptor.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.pwdb.kdbx.stream_3_1; - -import org.linguafranca.pwdb.kdbx.StreamEncryptor; -import org.linguafranca.pwdb.security.Encryption; -import org.spongycastle.crypto.engines.Salsa20Engine; -import org.spongycastle.crypto.params.KeyParameter; -import org.spongycastle.crypto.params.ParametersWithIV; -import org.spongycastle.util.encoders.Hex; - -import java.security.MessageDigest; - -/** - * A helper class for Salsa20 encryption. - * - *

    Salsa20 doesn't quite fit the memory model - * supposed by SerializableDatabase.StreamEncryptor - all encrypted - * items have to be en/decrypted in order of encryption, - * i.e. in document order and at the same time. - * - *

    The encrypt and decrypt methods - * actually do the same thing. They are here - * only to fulfill the interface contract. - * - * @author jo - */ -public class Salsa20StreamEncryptor implements StreamEncryptor { - - private final Salsa20Engine salsa20; - private final byte[] key; - - // Android compatibility - private static final byte[] SALSA20_IV = Hex.decode("E830094B97205D2A".getBytes()); - - /** - * Creates a Salsa20 engine - * - * @param key the key to use - * @return an initialized Salsa20 engine - */ - @SuppressWarnings("WeakerAccess") - public static Salsa20Engine createSalsa20(byte[] key) { - MessageDigest md = Encryption.getMessageDigestInstance(); - KeyParameter keyParameter = new KeyParameter(md.digest(key)); - ParametersWithIV ivParameter = new ParametersWithIV(keyParameter, SALSA20_IV); - Salsa20Engine engine = new Salsa20Engine(); - engine.init(true, ivParameter); - return engine; - } - - /** - * Constructor creates engine used for both encryption and decryption - * - * @param key the key to use - */ - public Salsa20StreamEncryptor(byte[] key) { - this.key = key; - salsa20 = createSalsa20(key); - } - - @Override - public byte[] getKey() { - return key; - } - - @Override - public byte[] decrypt(byte[] encryptedText) { - byte[] output = new byte[encryptedText.length]; - salsa20.processBytes(encryptedText, 0, encryptedText.length, output, 0); - return output; - } - - @Override - public byte[] encrypt(byte[] decryptedText) { - byte[] output = new byte[decryptedText.length]; - salsa20.processBytes(decryptedText, 0, decryptedText.length, output, 0); - return output; - } -} diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java index c8c780e9..c4a5a6dc 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java @@ -17,8 +17,7 @@ package org.linguafranca.pwdb.kdbx; import org.junit.Test; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxHeader; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxSerializer; +import org.linguafranca.pwdb.kdbx.KdbxSerializer; import org.linguafranca.pwdb.Credentials; import java.io.InputStream; diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java index 14ff37e2..d3f686dd 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java @@ -17,8 +17,6 @@ package org.linguafranca.pwdb.kdbx; import org.junit.Test; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxHeader; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxSerializer; import org.linguafranca.pwdb.Credentials; import java.io.*; diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java new file mode 100644 index 00000000..3e75670d --- /dev/null +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java @@ -0,0 +1,87 @@ +package org.linguafranca.pwdb.kdbx.stream; + +import org.junit.Ignore; +import org.junit.Test; +import org.linguafranca.pwdb.hashedblock.HmacBlockInputStream; +import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.KdbxHeader; +import org.linguafranca.pwdb.kdbx.KdbxSerializer; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Scanner; + +/** + * @author jo + */ +public class KdbxSerializerTest { + + @Test + public void loadArgonheader() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); + KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); + System.out.println("Version " + header.getVersion()); + } + + @Test + public void getHmacStream() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); + KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); + System.out.println("Version " + header.getVersion()); + HmacBlockInputStream hmacBlockInputStream = new HmacBlockInputStream(inputStream, true); + byte [] buf = new byte [1024]; + int bytesRead; + while ((bytesRead = hmacBlockInputStream.read(buf)) != -1) { + System.out.println(bytesRead); + } + } + + @Test + public void loadAesheader() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-AES.kdbx"); + KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); + System.out.println("Version " + header.getVersion()); + } + + @Test + public void loadChaChaAesHeader() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha-AES.kdbx"); + KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); + System.out.println("Version " + header.getVersion()); + } + + @Test @Ignore + public void getDecryptedArgonInputStream() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); + InputStream is = KdbxSerializer.createUnencryptedInputStream(new KdbxCreds("123".getBytes()), new KdbxHeader(), inputStream); + Scanner s = new Scanner(is); + while (s.hasNext()) { + System.out.println(s.nextLine()); + } + s.close(); + } + + @Test + public void getDecryptedAesInputStream() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-AES.kdbx"); + InputStream is = KdbxSerializer.createUnencryptedInputStream(new KdbxCreds("123".getBytes()), new KdbxHeader(), inputStream); + Scanner s = new Scanner(is); + while (s.hasNext()) { + System.out.println(s.nextLine()); + } + s.close(); + } + + @Test + public void getDecryptedCHaChaInputStream() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha-AES.kdbx"); + InputStream is = KdbxSerializer.createUnencryptedInputStream(new KdbxCreds("123".getBytes()), new KdbxHeader(), inputStream); + Scanner s = new Scanner(is); + while (s.hasNext()) { + System.out.println(s.nextLine()); + } + s.close(); + + } + +} \ No newline at end of file diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/security/Salsa20EncryptionTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java similarity index 66% rename from kdbx/src/test/java/org/linguafranca/pwdb/security/Salsa20EncryptionTest.java rename to kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java index 5e504cf1..494efd77 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/security/Salsa20EncryptionTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java @@ -17,7 +17,8 @@ package org.linguafranca.pwdb.security; import org.junit.Test; -import org.linguafranca.pwdb.kdbx.stream_3_1.Salsa20StreamEncryptor; +import org.linguafranca.pwdb.kdbx.StreamEncryptor; +import org.linguafranca.pwdb.kdbx.StreamEncryptor.*; import java.security.SecureRandom; @@ -26,18 +27,32 @@ /** * @author jo */ -public class Salsa20EncryptionTest { +public class StreamEncryptionTest { @Test - public void encrypt() { + public void salsa20() { byte[] key = SecureRandom.getSeed(32); + StreamEncryptor ss = new Salsa20(key); + StreamEncryptor tt = new Salsa20(key); + + verifyTwoWay(ss, tt); + } + + @Test + public void chacha20() { + byte[] key = SecureRandom.getSeed(32); + StreamEncryptor ss = new ChaCha20(key); + StreamEncryptor tt = new ChaCha20(key); + + verifyTwoWay(ss, tt); + } + + private void verifyTwoWay(StreamEncryptor ss, StreamEncryptor tt) { - Salsa20StreamEncryptor ss = new Salsa20StreamEncryptor(key); byte[] e = ss.encrypt("new secret".getBytes()); byte[] f = ss.encrypt("secret 2".getBytes()); - Salsa20StreamEncryptor tt = new Salsa20StreamEncryptor(key); String s1 = new String(tt.encrypt(e)); String t1 = new String(tt.encrypt(f)); @@ -46,8 +61,6 @@ public void encrypt() { System.out.println(s1); System.out.println(t1); - - } } \ No newline at end of file diff --git a/pom.xml b/pom.xml index fbca0744..a29e9d9a 100644 --- a/pom.xml +++ b/pom.xml @@ -135,7 +135,7 @@ com.madgag.spongycastle core - 1.54.0.0 + 1.58.0.0 com.google.code.gson diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java index c30d2b17..b92eb2ab 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java @@ -17,11 +17,11 @@ package org.linguafranca.pwdb.kdbx.simple; import org.linguafranca.pwdb.base.AbstractDatabase; +import org.linguafranca.pwdb.kdbx.StreamEncryptor; import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxInputTransformer; import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxOutputTransformer; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxHeader; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxSerializer; -import org.linguafranca.pwdb.kdbx.stream_3_1.Salsa20StreamEncryptor; +import org.linguafranca.pwdb.kdbx.KdbxHeader; +import org.linguafranca.pwdb.kdbx.KdbxSerializer; import org.linguafranca.pwdb.kdbx.simple.converter.*; import org.linguafranca.pwdb.kdbx.simple.model.EntryClasses; import org.linguafranca.pwdb.kdbx.simple.model.KeePassFile; @@ -186,7 +186,7 @@ public static SimpleDatabase load(Credentials credentials, InputStream inputStre // decrypt the encrypted fields in the inner XML stream InputStream plainTextXmlStream = new XmlInputStreamFilter(kdbxInnerStream, - new KdbxInputTransformer(new Salsa20StreamEncryptor(kdbxHeader.getProtectedStreamKey()))); + new KdbxInputTransformer(new StreamEncryptor.Salsa20(kdbxHeader.getInnerRandomStreamKey()))); // read the now entirely decrypted stream into database KeePassFile result = getSerializer().read(KeePassFile.class, plainTextXmlStream); @@ -227,7 +227,7 @@ public void save(Credentials credentials, OutputStream outputStream) throws IOEx // encrypt the fields in the XML inner stream XmlOutputStreamFilter plainTextOutputStream = new XmlOutputStreamFilter(kdbxInnerStream, - new KdbxOutputTransformer(new Salsa20StreamEncryptor(kdbxHeader.getProtectedStreamKey()))); + new KdbxOutputTransformer(new StreamEncryptor.Salsa20(kdbxHeader.getInnerRandomStreamKey()))); // set up the "protected" attributes of fields that need inner stream encryption prepareForSave(keePassFile.root.group); diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java index b674508d..98f1bcf3 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java @@ -23,25 +23,23 @@ import javax.xml.stream.XMLStreamException; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.Charset; +import java.util.Scanner; /** * @author jo */ public class KdbxInnerInputStreamTest { - + // TODO what does this test, exactly? @Test public void test() throws XMLStreamException, IOException { InputStream is = getClass().getClassLoader().getResourceAsStream("ExampleDatabase.xml"); - XmlInputStreamFilter sxd = new XmlInputStreamFilter(is, new KdbxInputTransformer(new org.linguafranca.pwdb.kdbx.stream_3_1.Salsa20StreamEncryptor.None())); - int len; - do { - byte b[] = new byte[8096]; - len = sxd.read(b); - if (len >-1) - System.out.print(new String(b, 0, len, Charset.forName("UTF-8"))); - } while (len > -1); + XmlInputStreamFilter sxd = new XmlInputStreamFilter(is, new KdbxInputTransformer(new StreamEncryptor.None())); + Scanner s = new Scanner(is); + while (s.hasNext()) { + System.out.println(s.nextLine()); + } + s.close(); } } \ No newline at end of file diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java index 6dbcb6af..e9c80e36 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java @@ -20,10 +20,9 @@ import org.linguafranca.pwdb.checks.SaveAndReloadChecks; import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.kdbx.KdbxCreds; -import org.linguafranca.pwdb.kdbx.StreamFormat; import org.linguafranca.pwdb.Credentials; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxHeader; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxSerializer; +import org.linguafranca.pwdb.kdbx.KdbxHeader; +import org.linguafranca.pwdb.kdbx.KdbxSerializer; import java.io.*; diff --git a/test/src/main/resources/V4-AES-AES.kdbx b/test/src/main/resources/V4-AES-AES.kdbx new file mode 100755 index 0000000000000000000000000000000000000000..03c60ab45fd5acdf540049b036e09e3a8a718cf6 GIT binary patch literal 1966 zcmV;f2T}L~*`k_f`%AR}00RI55CAd3^5(yBLr}h01tDtuTK@wC0096100bZaX+n1V z{B8FWmq*zMVpzYw3$)C?sGm}%$*jH&{D-{)1t0+HxAx8bO0f=T1@+K_vMLV6=UgOE zHX0%+r`C|?A40PR2moN}00000000LN09S*Qlc%b^nTd~Wt>YoOHV7a9(psAXM9v&O zQJ%Mvdkz2_OLFM$3=d>d$ENYNjg)$?eKLT8k=CWX5on z&w4sySh7h91ONg60000401XNa3U4|y0l}7)XE-`c8f!oPWq3;gCa3fSnyu_&T%_?Z z@OD9vUZMk|VHf#`6!O0L2%d_}c<9d7E|7qPunkIF79STPPWnI3sBEPx_qPB7756%# z($Ba!(S>?;FDz|p{<9J9qxhgX|5Tp`j@Uq7R9G-Df!?3a=8yZ_=Kbq4opb>AKOal} zr#k9=)b=MR`G>#&4y3)59j&=xjTgQR2{JCkIuZrB>GZ-S=&?hlwf2iU!_03|vKG-d zK?XpXq6ehyrOZrz>5*Jn@if1ha%AKm(qXREszy|7_-|v#yF!5S{O{Tsf)|5zZl~^04ipa z#bk@3ow->(PKK?{urIJ?v-bd~f(-v|sjbw!nj&6aI~c5dS@j&35*Ft+w7RDHFBln=jIBfzs+cUc@-Hc0zGl&4PK2d_( zTXzuN7VDuaTa*{3$-_^as{o%1C|oZFZf?+7O&^XabwvSe>X~fMN)hIMJnUx+NA%~J zPox7^R5Jpc?i||+7}^qNXis@7fYcUX3P^}p4Y(y^Qono3(i@kV)SK-qMdQm@^S z#VNtqv@N`Z?fLqi%M>?S#jssyjwe-o$l=prsi7Mxu!whyDLyX*zWQGfANhY?-Y~*x zP{WK(%7uhpi}#crP(E&e?JKNjy8)px&;p($>q3?Y?dgSUt83pH%X^G=dpt}1`E+V- z;W&leoIFW<#8856iN{Y}o;370QInAkH|NFe$iGXyDj+kc1BdQWz>ZUW*z>`O@r7v( z8A<=Tjd-ta)-}4#gr^EK3?uT*w)~j|2NIx_Jv-V^oe0*ORE*DFxhNe(r1t^90QFbk zK4D0bbk=)DfX(hOdrN=F%mP>|r>TQ7pW!G%{lQPC=~%<>q_-x5JkuJp?!g|eXb_ zV}$4q2m!oq;EMa3sj4ux$C2!_GBkqT*jdG!qVoy;P;->D+sr@wo?8bgp?BeYLdnQs zN!qwg0kz2!NiMgIf?BOweAgf#&8Ypgi`uP&QVBQf!WK+57QOUz%83kWgl$Pw(Vrvy zc98q`<55QWQ9{{_Ba;ZK^hNsP{6LPCxvstG40EyjEvd1B&Xp(kBOuiXT@Np54|N86 zfr+6j2aZ@4J{x0Nv_;VD1QcHf*nh!;#W=aakt`coEfVggb*e2(+qXhIY$U+>6WpEKVUA6d z0%KZxt9zOzAEua*mnPS>*un0~TW!F#6oy)oK!G>I%+mf3g-ZE=1$_#JK^P-* zD?j(s@&0$pM-+egDf6NN@8JOx2KcFtIf*SjXv>7kY&@a>!Hr-DOYKpULm1i-8q;%s@F5sVyCuDWcmqIOf$ui z|E!*+K<7DGimAqEkd<{3#FDAzIh=3?UM=$sGb*ZW6}8n+a literal 0 HcmV?d00001 diff --git a/test/src/main/resources/V4-AES-Argon2.kdbx b/test/src/main/resources/V4-AES-Argon2.kdbx new file mode 100755 index 0000000000000000000000000000000000000000..5d9e5794dcb1e1d3132cf5deb6cd54cdf25ed608 GIT binary patch literal 2005 zcmV;`2P*gj*`k_f`%AR|00aO65C8xGF~RcYzi~rQzE}kzYW!ON0|Wp70096100bZa z004prcj3G+nzvL_0%l$7S7HkaJzDp>#jD-9_8&G09F7Z%000000YU`;001OaRY^n; z0002*V{PAzDMU+=_o<`<;|dG}0RR91Rs;Y5022TJ00jX6002n{000020000000005 z0RR91O$Y!00000G0000000aR5002+~000020000&0RR91Qy>5U0O*ze#$VE-%P5+!EHA~^k|257?o1ONa44GIkk z#enBo2_H4T9FO--c**^~WIxHeD^&}9E}HErvCYoIwUfM7m{CKUFk&4!!m+t$=PFLV ze;Y?qheFMNdY3_!e-t;weNty18rQya;f`;4Tyy}1SBZoMVXzfi_85xjuYs!Uj}(h~ogF~r^S zBGR7V(20&XIlVP7HSU*Nx!E$rH@)NKucX2mD8Bv6Ds;? z|I{U2UlgB0jV~f);l5Ez9ArDSRoy@^+gEn5ds3_VxMXEffFH|gaZbB?D&)+uUl5v3OhH-OgXLu*%3Ta~!>#iR~c)rI`orDOfooNO|DCyAjiGff5Jt zDI2UIWrv8$OGr!4J6osuyVwc%+|wb| zW)P|mQzT!G`zF?TPkvi4s)K(cX29ZqBwPEce1!%SNdaN}(}xAWPQTVLy}l*Kk3V+4 ziev7Jy;bVU?FMBu3}Q9G|0Wjh+9S#8yBw!-k@%R_C&W@5n0}x^G;4rRW9T}Oy3Lcx z)h^@tb@h`EHYfhWvwB^`oHK=yOGo}&A#XX)VLaL zQ-o(T^14%tj>rD?sDZ^7$6P6<9XRI{&O^58&ubArJy5UhjvAJGpbXa~qh}A~M_$Eo zmr~Juq?Op?IOMH7J|Y8tI(|p-jT}Dc?gQXHbtA0U;NJ9gw6#44euBwd%@>cyNkCL} ztu#)nN&GH!c)p$2ZLSN7i;?qouRN4^!V3He2*bK9ys?qvNAgoebBHV`i_zGE5!dmW ze_;RYQ8}c{!$Df^(5OSf>LTKF{Hmx+s;=-*z@Z&totwaomm0rUV%f{XN|RwS7`npK zaqXcd9@rI=$h59?VT?17#7M7Ma(LFX93}RY`f07YTwx!}fay-pXI~C;m>h`9Aony7 ztZ52R;|Ae#xu(I(i4K!i->TOvv!>?Z^&hqSmLlDssO`|EUH_9tjEf6`B;aTuI)U#4 z6!y0GRW7!eG9?#!Sl}_U$?dMoU^?{#tHSK>Z8@k@6uUU!qLYtq8`0hT$|mmg;X^wP z0nlj0GE-z0&B4Gm5QQM=nA&~g=R*4C(>|b$O+tMmc1+>-6gxRrOSt(T0|3U+RiMA` zylei+K$3UzjTLq&FE3+t6}RawN=iZYXP@q3Z(kmgRTItS%Bw-AX2Hqi!oaj_}DWv zjh51LSL`?t$JdZubbKB3kCehn-)%w!X7^jNY0%NirZ#UIsc_1_X literal 0 HcmV?d00001 diff --git a/test/src/main/resources/V4-ChaCha-AES.kdbx b/test/src/main/resources/V4-ChaCha-AES.kdbx new file mode 100755 index 0000000000000000000000000000000000000000..c3e212f8c7157b2d5a9f4f70c55014a76c1533b7 GIT binary patch literal 1958 zcmV;X2U++7*`k_f`%AR}00RI55CGN#iYtq6OtqyXGnz5mwVDG20096100bZaIT41L zw9M)<^5GX#I=k9RT@XH`?c^+v#MpGp+02P?1t0)SInY>OTy23C86MKJ(Zu!ny5W9M z_01eysFgLO)IiAw2moN}00000000LJ07@oc_tBCQpIa+N&p-WeTYy0;e2_OJ#%TKaL-3O)x8A|P{D_}r4_YOykF#cH=OJUue z=FW5q1ONg60000401XNa3KqCRn3ZIgM6HJl=K zGLrhWNgn9PkN35%T+R!B&JE8$x5awSea7MdHfm;JNVU-@>=(`1k~}tr&pD0NXoH2w znG|8|_R8|#K8<&2s9ujKrfXPj3)~pK#^vxEvXO-P)#{X#X)W0tWfPoabz^GGO7Gie zNY(jjInO*Q5(u;4(vfdc1Kl81Qw!mk(7=wL=}eDBtm7_eW!FxjJ!7v$pipnQ$9Cu` z^5{re(`SN?bK?$_8d31VRfkcww9MwORqh285l60RpSzy7y|OJ@^w1TRZzSv?*kRiH zb({i`Of;cW&qI`er>bjq=4BzJURp4Ri;tV;dHuCS74!iyRgABse-nBxX@B_^1hWJD zhKNoY3kf}ODe!hdphBK>>SAy%!HO<2=_d>25+rbW4)BBig6au-)IIQE#Fz*Xt^wXJ zTyX^O5ssb-S9G?A;Q$D@uM~nTdj6~MSf;QSJmAR5P;Aq$Uduqwq8c0W*VO^|0uOT9 zjuN=Q*$`2Mv=c={xS0?hN{t*Tu3gNb>ER zQU>?83>FzJ?N#{Yno`W-%DXSY2K*nn4jmr+sALO`plh6h@yDpkBu#sX4sJBStX*bQ z(xvmbq7!`{=zuL(aHr}KZr7cs@vP5gR5<(I-xE2@7^K0J5F=n{jhF8sl`6C3?#cwc z6Yg_}F*EpJMvhrqrV`b>JT-A;7Lvd6LOV_1Aq=vvD;xq$>_-AuOOKYe^(5j#o&jqy zZewriYA>-6O7jU2wWuAe#Oncj2oPUcVJ?&G<1>IQXggs{+^N(E&xMqF9hg6h4vi6YlcC>S#aiUMR2Bn^!b&PK~zPNXo*ll0}sbtDFi5aET^-<8!hl`Mu|2nT? zA*fS8ua6Gx-C0P825&LD? zSoQa2dgPJ^mbwBryZnQU-fJoZy73ZP!8biYP;1DKaOUUgBQTqS3)Vzx)q6~-3}%k!pK=C%}; zLuuX8WLYH!?uD%bKT#c6AgsAo__2H_KGALNKyXx?kcCdZ<2O=8xf~Ry$^thz~DF+=dZB31zp3IvJNBhE+B9R+04}zTg)|0QHHs z+x8QGJX*sm<_0oHc;5c$c9b=m2Phy5 z;HIAU`Q@aW0q~CuWO%^!EYcP?fzdwOXu)(dRKV_%Q5D|Q2^ zYoAb@I)OeJbS<&5kORp&03E%T9bl4zIom4@i^rk|z~2!)t^JyzO|ek&5NT2V*krjJ z8tNsgbPX2_>-kt2tNQ20iKj2o`K3=Z;6hJ+vK`y0PqkdaafD|vQ+Hj`wrRb(v8JKo zPjEthvUwKAv@^Vj4_i{9u81GWA3a^Wf|%yVH3%yKrL=$`nMQzAs347H^~~o=CC}Un s0vr8zhsbo+eQwNZwFkpBdSj<1vJaUuP?tE<8D|qD$%?_=H@RKB2gI?uqyPW_ literal 0 HcmV?d00001 From bec45b850b67b824c040635630b9e3c8041e5559 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 29 Jan 2018 13:48:36 +0000 Subject: [PATCH 057/228] Updating to version 2.2-SNAPSHOT --- .gitignore | 3 ++- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- http/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 2 +- simple/pom.xml | 2 +- test/pom.xml | 2 +- 12 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index c601f37f..c5d45fa4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ settings.gradle gradlew gradlew.bat .gradle -gradle \ No newline at end of file +gradle +*.versionsBackup diff --git a/all/pom.xml b/all/pom.xml index 3ac4774c..a7f91c5f 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.2-SNAPSHOT ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index d08c3303..64a4e81b 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index f35731a2..5e9d4864 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index f0a94c94..1ced0830 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/http/pom.xml b/http/pom.xml index a6fff8d3..7757678e 100644 --- a/http/pom.xml +++ b/http/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 3acc7338..872d36d6 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index b1ce06f1..df4b4533 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index ccab40c3..f727bc85 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index a29e9d9a..22f44ffc 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.1.4-SNAPSHOT + 2.2-SNAPSHOT database test diff --git a/simple/pom.xml b/simple/pom.xml index c17da12c..9400097a 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index 63bc46cb..154ca957 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.2-SNAPSHOT 4.0.0 From 9ee864a117fe908038d4a854a3d2c0a1c3be1e5f Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 29 Jan 2018 15:33:54 +0000 Subject: [PATCH 058/228] Updating documentation --- CHANGELOG.md | 6 ++++++ Format Diagram.svg | 2 -- FormatDiagram.svg | 3 +++ Module Structure.svg => ModuleStructure.svg | 0 .../org/linguafranca/pwdb/security/Aes.java | 4 ++++ .../org/linguafranca/pwdb/security/Argon.java | 18 +++++++--------- .../linguafranca/pwdb/security/ChaCha.java | 2 ++ .../pwdb/security/VariantDictionary.java | 2 +- .../pwdb/kdbx/KdbxSerializer.java | 21 ++++++++++++++----- readme.md | 11 ++++++---- 10 files changed, 47 insertions(+), 22 deletions(-) delete mode 100644 Format Diagram.svg create mode 100644 FormatDiagram.svg rename Module Structure.svg => ModuleStructure.svg (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index c78f106b..46cfbdae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.com) and [Semantic Versioning](http://semver.org/spec/v2.0.0.html) +##[2.2-Snapshot] + +### Added + +- Preliminary readonly support for V4 files + ##[2.1.4-Snapshot] ### Added diff --git a/Format Diagram.svg b/Format Diagram.svg deleted file mode 100644 index 86b27bea..00000000 --- a/Format Diagram.svg +++ /dev/null @@ -1,2 +0,0 @@ - -
    Hashed Blocks
    Hashed Blocks
    Magic Number
    Magic Number
    Version
    Version
    Header
    Header
    Header
    Header
    Header
    Header
    Header 0
    Header 0
    ...
    ...
    Value of Header 9
    Value of Header 9
    0
    0
    8
    8
    4
    4
    Type
    Type
    Length
    Length
    Value
    Value
    Encrypted
    Encrypted
    Unencrypted
    Unencrypted
    Sequence
    Sequence
    MD5 Hash
    MD5 Hash
    Length
    Length
    KDBX 3.1 File
    KDBX 3.1 File
    Header Format
    Header Format
    Hashed Block
    Format
    Hashed Block<div>Format</div>
    Unencrypted
    Payload
    (Optionally Compressed)
    [Not supported by viewer]
    36
    36
    0
    0
    40
    40
    12
    12
    1
    1
    3
    3
    0
    0
    Byte
    Offset
    [Not supported by viewer]
    Byte
    Offset
    [Not supported by viewer]
    Byte
    Offset
    [Not supported by viewer]
    Hashed Blocks
    Hashed Blocks
    Magic Number
    Magic Number
    Version
    Version
    Header
    Header
    Header
    Header
    Header
    Header
    Header 0
    Header 0
    ...
    ...
    0
    0
    8
    8
    32
    32
    Type
    Type
    Length
    Length
    Value
    Value
    Unencrypted
    Unencrypted
    Length
    Length
    Header Format
    Header Format
    Hashed Block
    Format
    Hashed Block<div>Format</div>
    HMAC-SHA-256
    HMAC-SHA-256
    36
    36
    0
    0
    12
    12
    1
    1
    5
    5
    0
    0
    Byte
    Offset
    [Not supported by viewer]
    Byte
    Offset
    [Not supported by viewer]
    Byte
    Offset
    [Not supported by viewer]
    KDBX 4 File
    KDBX 4 File
    http://keepass.info/help/kb/kdbx_4.html
    http://keepass.info/help/kb/kdbx_4.html
    HMAC-SHA-256
    HMAC-SHA-256
    Encrypted
    Payload
    (Optionally Compressed)
    [Not supported by viewer]
    \ No newline at end of file diff --git a/FormatDiagram.svg b/FormatDiagram.svg new file mode 100644 index 00000000..944cdec0 --- /dev/null +++ b/FormatDiagram.svg @@ -0,0 +1,3 @@ + + +
    Magic Number
    Magic Number
    Version
    Version
    Header
    Header
    Header
    Header
    Header
    Header
    Header 0
    Header 0
    ...
    ...
    Value of Header 9
    Value of Header 9
    0
    0
    8
    8
    4
    4
    Type
    Type
    Length
    Length
    Value
    Value
    Encrypted
    Encrypted
    Unencrypted
    Unencrypted
    Sequence
    Sequence
    MD5 Hash
    MD5 Hash
    Length
    Length
    KDBX 3.1 File
    KDBX 3.1 File
    Header Format
    Header Format
    Hashed Block
    Format
    Hashed Block<div>Format</div>
    Unencrypted
    Payload
    (Optionally Compressed)
    [Not supported by viewer]
    36
    36
    0
    0
    40
    40
    12
    12
    1
    1
    3
    3
    0
    0
    Byte
    Offset
    [Not supported by viewer]
    Byte
    Offset
    [Not supported by viewer]
    Byte
    Offset
    [Not supported by viewer]
    Magic Number
    Magic Number
    Version
    Version
    Header
    Header
    Header
    Header
    Header
    Header
    Header 0
    Header 0
    ...
    ...
    0
    0
    8
    8
    32
    32
    Type
    Type
    Length
    Length
    Value
    Value
    Unencrypted
    Unencrypted
    Length
    Length
    Header Format
    Header Format
    Hashed Block
    Format
    Hashed Block<div>Format</div>
    HMAC-SHA-256
    HMAC-SHA-256
    36
    36
    0
    0
    12
    12
    1
    1
    5
    5
    0
    0
    Byte
    Offset
    [Not supported by viewer]
    Byte
    Offset
    [Not supported by viewer]
    Byte
    Offset
    [Not supported by viewer]
    KDBX 4 File
    KDBX 4 File
    http://keepass.info/help/kb/kdbx_4.html
    http://keepass.info/help/kb/kdbx_4.html
    SHA-256
    SHA-256
    Encrypted
    Payload
    (Optionally Compressed)
    [Not supported by viewer]
    HMAC-SHA-256
    HMAC-SHA-256
    Inner Header
    Inner Header
    Hashed Blocks
    Hashed Blocks
    Inner Header
    Inner Header
    ...
    ...
    Header 0
    Header 0
    XML Database
    XML Database
    Hashed Blocks
    Hashed Blocks
    XML Database
    XML Database
    \ No newline at end of file diff --git a/Module Structure.svg b/ModuleStructure.svg similarity index 100% rename from Module Structure.svg rename to ModuleStructure.svg diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java index 9f3f6561..12c181ea 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java @@ -11,12 +11,16 @@ import static org.linguafranca.pwdb.security.Encryption.getSha256MessageDigestInstance; /** + * AES may be used for Key Derivation and aslo as the underlying stream cipher + * * @author jo */ public class Aes { + /** UUID specifying that AES is to be used as the Key Derivation Function */ public static final UUID KDF = UUID.fromString("C9D9F39A-628A-4460-BF74-0D08C18A4FEA"); + /** v4 variant dictionary keys for use of AES as the KDF */ public static class KdfKeys { public static final String ParamRounds = "R"; // UInt64 public static final String ParamSeed = "S"; // Byte[32] diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Argon.java b/database/src/main/java/org/linguafranca/pwdb/security/Argon.java index 389c9ece..96e9b664 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Argon.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Argon.java @@ -9,12 +9,16 @@ /** + * KDBX V4 files may use Argon2 for key derivation. + * * @author jo */ public class Argon { + /** UUID indicating that Argon is being used as the KDF */ public static final UUID argon2_kdf = UUID.fromString("EF636DDF-8C29-444B-91F7-A9A403E30A0C"); + /** keys into the variant dictionary supplied as a KDBX header */ public static class ArgonParameterKeys { @SuppressWarnings("unused") public static class ArgonKeys { @@ -40,10 +44,6 @@ public static class ArgonKeys { int parallelism = argonParameterKeys.get(paramParallelism).asInteger(); int memoryCost = (int) argonParameterKeys.get(paramMemory).asLong(); int timeCost = (int) argonParameterKeys.get(paramIterations).asLong(); -/* - byte [] secretKey = argonParameterKeys.entries.get(paramSecretKey).value; - byte [] assocData = argonParameterKeys.entries.get(paramAssocData).value; -*/ // Configure the hasher Hasher hasher = jargon2Hasher() @@ -51,18 +51,16 @@ public static class ArgonKeys { .version(version) .salt(salt) .parallelism(parallelism) - .memoryCost(memoryCost/1024) + .memoryCost(memoryCost/1024) // block size 1024 .timeCost(timeCost) .hashLength(32); - + // do the hash byte [] hash = hasher.password(digest).rawHash(); + // return digest of master seed and hash MessageDigest md = getSha256MessageDigestInstance(); - byte [] transformedKeyDigest = hash; // md.digest(hash); - md.update(masterSeed); - return md.digest(transformedKeyDigest); - + return md.digest(hash); } } diff --git a/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java b/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java index 38f0b409..a929397b 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java @@ -4,6 +4,8 @@ import org.spongycastle.crypto.engines.ChaCha7539Engine; /** + * ChaCha20 may be used for the inner stream as well as for the underlying database encryption scheme + * * @author jo */ public class ChaCha { diff --git a/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java b/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java index 3f8ed67e..3b329342 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java @@ -9,7 +9,7 @@ import java.util.UUID; /** - * Implementation of a storage for KDBX Header field parameters + * Implementation of a storage for V4 KDBX Header field parameters * * @author jo */ diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java index 22d96b79..94dfb5f5 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java @@ -36,9 +36,9 @@ import java.util.zip.GZIPOutputStream; /** - * This class provides static methods for the encryption and decryption of Keepass KDBX V4 files. + * This class provides static methods for the encryption and decryption of Keepass KDBX V3 and V4 files. *

    - * A KDBX file is little-endian and consists of the following: + * A KDBX files are little-endian and consist of the following: *

      *
    1. An unencrypted portion
    2. *
        @@ -46,11 +46,13 @@ *
      1. 4 bytes version
      2. *
      3. A header containing details of the encryption of the remainder of the file
      4. *

        The header fields are encoded using a TLV style. The Type is an enumeration encoded in 1 byte. - * The length is encoded in 2 bytes and the value according to the length denoted. The sequence is - * terminated by a zero type with 0 length.

        + * The length is encoded in 4 bytes (V3: 2 bytes) and the value according to the length denoted. The sequence is + * terminated by a zero type with 0 length. {@link #readOuterHeader}

        + *

        In V4 there follows a 32 byte SHA-256 hash of the file so far

        + *

        In V4 there follows a 32 byte HMAC-256 hash of the file so far

        *

        {@link KdbxHeader} details the fields of the header.

        *
      - *
    3. An encrypted portion
    4. + *
    5. In V3 the remainder of the file is encrypted as follows:
    6. *
        *
      1. A sequence of bytes contained in the header. If they don't match, decryption has not worked.
      2. *
      3. A payload serialized in Hashed Block format, see e.g. {@link HashedBlockInputStream} for details of this.
      4. @@ -58,6 +60,14 @@ *
      5. The content is now a character stream, which is expected to be * XML representing a KeePass Database. Assumed UTF-8 encoding.
      6. *
      + *
    7. In V4 the remainder of the file is encoded as HMacHasedBlocks:
    8. + *
        + *
      1. A sequence of blocks encoded using Hmac Blocks see {@link HmacBlockInputStream}
      2. + *
      3. Those blocks contain an encrypted input stream.
      4. + *
      5. The encrypted input stream optionally contains a Gzipped input stream.
      6. + *
      7. The content is now a character stream, which is an inner header {@link #readInnerHeader} + * followed by XML representing a KeePass Database. Assumed UTF-8 encoding.
      8. + *
      *
    *

    The methods in this class provide support for serializing and deserializing plain text payload content * to and from the above encrypted format. @@ -66,6 +76,7 @@ * are stream encrypted, meaning they have to be decrypted in the * same order as they were encrypted, namely actual XML document order. Or at least that * is the way it seems. The methods of this class do not perform this aspect of encryption/decryption. + * @see this diagram * @author jo */ @SuppressWarnings("WeakerAccess") diff --git a/readme.md b/readme.md index 115d5fc2..184ccb5b 100644 --- a/readme.md +++ b/readme.md @@ -8,7 +8,7 @@ safe for Windows. Features to date: -- Read and write KeePass 2.x format +- Read and write KeePass 2.x format (File format V3 supported, V4 in development) - Keepass 2.x Password and Keyfile Credentials - Read KeePass 1.x format (Rijndael only) - *No* requirement for JCE Policy Files @@ -124,9 +124,11 @@ KeePass is in effect defined by the code that Dominik writes to create and maint Hence there are no definitive specification of KeePass files other than that code. For the sake of clarification and my own satisfaction I have written about my understanding of KeePass formats in the following locations: -1. The Javadoc header to [KdbxSerializer](kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.java) describes KDBX stream formatting. +1. The Javadoc header to [KdbxSerializer](kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java) describes KDBX stream formatting. 2. The XSD Schema [KDBX.3.1.xsd](KDBX.3.1.xsd) documents my understanding of the Keepass XML, and also my lack of understanding, in parts. -3. [This graphic](Format Diagram.svg) illustrates KDBX 3.1 stream format and also illustrates proposals for the revised KDBX 4.0 format. +3. The following graphic illustrates KDBX 3.1 and 4 file formats: + +[![This graphic](FormatDiagram.svg)](FormatDiagram.svg) ## Dependencies @@ -135,6 +137,7 @@ Aside from the JRE the API depends on: - [Google Guava](https://github.com/google/guava/wiki) ([Apache 2 license](https://github.com/google/guava/blob/master/COPYING)). - [Apache Commons Codec](https://commons.apache.org/proper/commons-codec/) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0)). - [Spongy Castle](https://rtyley.github.io/spongycastle/) which is a repackaging for Android of [Bouncy Castle](https://www.bouncycastle.org/java.html) ([Apache 2 license](https://www.bouncycastle.org/licence.html)). +- [Jargon2](https://github.com/kosprov/jargon2-api) for KDBX format version 4 Argon2 Key Derivation Function (Apache2 license) The Simple XML implementation additionally depends on: @@ -151,7 +154,7 @@ Included POM is for Maven 3. There are rather a lot of modules, this is in order to allow loading of minimal necessary functionality. The module dependencies are illustrated below. -[![Module Structure](./Module Structure.svg "Module Structure")](./Module Structure.svg) +[![Module Structure](ModuleStructure.svg "Module Structure")](./ModuleStructure.svg) Each module corresponds to a Maven artifact. The GroupId is `org.linguafranca.pwdb`. The version id is as noted [above](#mvn). From 658f6ab3a52f87eab1718518638cd6f105d82138 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 30 Jan 2018 19:26:27 +0000 Subject: [PATCH 059/228] V4 HMAC verficiation now working on input --- .../org/linguafranca/pwdb/security/Aes.java | 11 +-- .../org/linguafranca/pwdb/security/Argon.java | 12 ++-- .../org/linguafranca/pwdb/kdb/KdbHeader.java | 8 ++- .../hashedblock/CollectingInputStream.java | 68 ++++++++++++++++++ .../hashedblock/HmacBlockInputStream.java | 72 ++++++++++++++++--- .../linguafranca/pwdb/kdbx/KdbxHeader.java | 46 +++++++++--- .../pwdb/kdbx/KdbxSerializer.java | 64 ++++++++++++----- .../pwdb/kdbx/stream/KdbxSerializerTest.java | 5 +- 8 files changed, 233 insertions(+), 53 deletions(-) create mode 100644 kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingInputStream.java diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java index 12c181ea..3f5a7f60 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java @@ -29,10 +29,8 @@ public static class KdfKeys { public static PaddedBufferedBlockCipher getCipher() { return new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine())); } - /** - * Create a final key from the parameters passed - */ - public static byte[] getFinalKeyDigest(byte[] key, byte[] masterSeed, byte[] transformSeed, long transformRounds) { + + public static byte [] getTransformedKey (byte [] key, byte [] transformSeed, long transformRounds) { AESEngine engine = new AESEngine(); engine.init(true, new KeyParameter(transformSeed)); @@ -48,9 +46,6 @@ public static byte[] getFinalKeyDigest(byte[] key, byte[] masterSeed, byte[] tra } MessageDigest md = getSha256MessageDigestInstance(); - byte[] transformedKeyDigest = md.digest(transformedKey); - - md.update(masterSeed); - return md.digest(transformedKeyDigest); + return md.digest(transformedKey); } } diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Argon.java b/database/src/main/java/org/linguafranca/pwdb/security/Argon.java index 96e9b664..847ceddd 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Argon.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Argon.java @@ -1,5 +1,7 @@ package org.linguafranca.pwdb.security; +import com.kosprov.jargon2.api.Jargon2; + import java.security.MessageDigest; import java.util.UUID; @@ -35,9 +37,8 @@ public static class ArgonKeys { } } - @SuppressWarnings("ConstantConditions") - public static byte [] getArgonFinalKeyDigest(byte [] digest, byte [] masterSeed, VariantDictionary argonParameterKeys) { + public static byte[] getTransformedKey(byte[] digest, VariantDictionary argonParameterKeys) { byte bVersion = argonParameterKeys.get(paramVersion).asByteArray()[0]; Version version = bVersion == 0x13 ? Version.V13 : Version.V10; byte [] salt = argonParameterKeys.get(paramSalt).asByteArray(); @@ -56,11 +57,6 @@ public static class ArgonKeys { .hashLength(32); // do the hash - byte [] hash = hasher.password(digest).rawHash(); - - // return digest of master seed and hash - MessageDigest md = getSha256MessageDigestInstance(); - md.update(masterSeed); - return md.digest(hash); + return hasher.password(digest).rawHash(); } } diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbHeader.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbHeader.java index 4efc1307..0a67633a 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbHeader.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbHeader.java @@ -22,6 +22,9 @@ import javax.crypto.Cipher; import java.io.IOException; import java.io.InputStream; +import java.security.MessageDigest; + +import static org.linguafranca.pwdb.security.Encryption.getSha256MessageDigestInstance; /** * This class stores the encryption details of a KDB file and provides a method to create @@ -62,7 +65,10 @@ public InputStream createDecryptedInputStream(byte[] key, InputStream inputStrea throw new IllegalStateException("StreamEncryptor algorithm is not supported"); } - byte[] finalKeyDigest = Aes.getFinalKeyDigest(key, masterSeed, transformSeed, transformRounds); + byte[] transformedKeyDigest = Aes.getTransformedKey(key, transformSeed, transformRounds); + MessageDigest md = getSha256MessageDigestInstance(); + md.update(masterSeed); + byte[] finalKeyDigest = md.digest(transformedKeyDigest); return Encryption.getDecryptedInputStream(inputStream, Aes.getCipher(), finalKeyDigest, encryptionIv); } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingInputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingInputStream.java new file mode 100644 index 00000000..9dd32ee7 --- /dev/null +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingInputStream.java @@ -0,0 +1,68 @@ +package org.linguafranca.pwdb.hashedblock; + +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Class allows the forwarding and collection of read bytes as a buffer - e.g. to provide for HMAC operations + * + * @author jo + */ +public class CollectingInputStream extends FilterInputStream { + + private ByteArrayOutputStream collectedBytes = new ByteArrayOutputStream(); + private boolean collecting = true; + + protected CollectingInputStream(InputStream in) { + this(in, true); + } + + public CollectingInputStream(InputStream in, boolean collecting) { + super(in); + this.collecting = collecting; + } + + @Override + public int read() throws IOException { + int result = super.read(); + if (collecting && result != -1) { + collectedBytes.write(result); + } + return result; + } + + @Override + public int read(@NotNull byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(@NotNull byte[] b, int off, int len) throws IOException { + int result = super.read(b, off, len); + if (collecting && result != -1) { + collectedBytes.write(b, off, result); + } + return result; + } + + @Override + public long skip(long n) throws IOException { + return super.skip(n); + } + + public byte [] getCollectedBytes() { + return collectedBytes.toByteArray(); + } + + public boolean isCollecting() { + return collecting; + } + + public void setCollecting(boolean collecting) { + this.collecting = collecting; + } +} diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java index 9594cd85..1dba6fd4 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java @@ -2,8 +2,16 @@ import com.google.common.io.LittleEndianDataInputStream; import org.jetbrains.annotations.NotNull; +import org.linguafranca.pwdb.security.Encryption; +import org.spongycastle.crypto.digests.SHA256Digest; +import org.spongycastle.crypto.macs.HMac; +import org.spongycastle.crypto.params.KeyParameter; import java.io.*; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.MessageDigest; +import java.util.Arrays; /** * Takes an underlying stream formatted as HMAC Hashed Blocks and provides @@ -13,19 +21,21 @@ */ public class HmacBlockInputStream extends FilterInputStream { - private final boolean littleEndian; + private final ByteOrder byteOrder; + private final byte[] key; private volatile ByteArrayInputStream bufferStream; private final DataInput input; private boolean finished; private int blockCount = 0; - public HmacBlockInputStream(InputStream inputStream) throws IOException { - this(inputStream, false); + public HmacBlockInputStream(byte [] key, InputStream inputStream) throws IOException { + this(key, inputStream, false); } - public HmacBlockInputStream(InputStream inputStream, boolean littleEndian) throws IOException { + public HmacBlockInputStream(byte [] key, InputStream inputStream, boolean littleEndian) throws IOException { super(inputStream); - this.littleEndian = littleEndian; + this.byteOrder = littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN; + this.key = key; if (littleEndian) { input = new LittleEndianDataInputStream(in); } else { @@ -49,15 +59,61 @@ private void getBlock() throws IOException { byte [] buffer = new byte [blockSize]; input.readFully(buffer); - verifyHmac(buffer); + verifyHmac(buffer, hmacSha256, blockCount); // create a new internal stream for the block bufferStream = new ByteArrayInputStream(buffer); blockCount ++; } - private void verifyHmac(byte[] buffer) { - // TODO actually verify the HMAC + /** + * HmacBlockStream.cs ReadSafeBlock + * @param buffer + * @param hmacSha256 + * @param blockNumber + */ + private void verifyHmac(byte[] buffer, byte[] hmacSha256, long blockNumber) { + HMac hmac = new HMac(new SHA256Digest()); + hmac.init(new KeyParameter(getHmacBlockKey(key, blockNumber, byteOrder))); + hmac.update(toBytes(blockNumber, byteOrder), 0, 8); + hmac.update(toBytes(buffer.length, byteOrder), 0, 4); + hmac.update(buffer, 0, buffer.length); + hmac.update(new byte[0], 0, 0); + byte[] computedHmacSha256 = new byte[32]; + hmac.doFinal(computedHmacSha256, 0); + if (!Arrays.equals(computedHmacSha256, hmacSha256)) { + throw new IllegalStateException("Block HMAC does not match"); + } + } + + /** + * From HmacBlockStream.cs GetHmacKey64 + * Calculates the block key for the block number ... + * @param key the HMAC key + * @param blockIndex the block number + * @param order Byte order to use + * @return a key + */ + public static byte [] getHmacBlockKey(byte [] key, long blockIndex, ByteOrder order) { + MessageDigest md = Encryption.getSha512MessageDigestInstance(); + md.update(toBytes(blockIndex, order)); + return md.digest(key); + } + + private static byte[] toBytes(long value, ByteOrder byteOrder) { + byte[] longBuffer = new byte [8]; + ByteBuffer.wrap(longBuffer) + .order(byteOrder) + .putLong(value); + return longBuffer; + } + + private static byte[] toBytes(int value, ByteOrder byteOrder) { + byte[] longBuffer = new byte [4]; + ByteBuffer.wrap(longBuffer) + .order(ByteOrder.LITTLE_ENDIAN) + .putInt(value); + return longBuffer; } @Override diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java index 28c59f32..5956036f 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java @@ -22,12 +22,15 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; +import java.security.MessageDigest; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.UUID; +import static org.linguafranca.pwdb.security.Encryption.getSha256MessageDigestInstance; + /** * This class represents the header portion of a KeePass KDBX file or stream. The header is received in * plain text and describes the encryption and compression of the remainder of the file. @@ -99,6 +102,8 @@ public enum ProtectedStreamAlgorithm { * on transmission or receipt */ private byte[] headerHash; + private byte[] headerBytes; + /** * Construct a default KDBX header */ @@ -126,7 +131,21 @@ public KdbxHeader() { * @throws IOException if something bad happens */ public InputStream createDecryptedStream(byte[] digest, InputStream inputStream) throws IOException { - byte[] finalKeyDigest; + // return digest of master seed and hash + MessageDigest md = getSha256MessageDigestInstance(); + md.update(masterSeed); + byte[] finalKeyDigest = md.digest(getTransformedKeyDigest(digest)); + + if (AES_CIPHER.equals(cipherUuid)) { + return Encryption.getDecryptedInputStream(inputStream, Aes.getCipher(), finalKeyDigest, getEncryptionIv()); + } else if (CHACHA_CIPHER.equals(cipherUuid)) { + return Encryption.getDecryptedInputStream(inputStream, ChaCha.getCipher(), finalKeyDigest, getEncryptionIv()); + } + throw new UnsupportedOperationException("Unknown encryption cipher " + cipherUuid); + } + + public byte[] getTransformedKeyDigest(byte[] digest) { + byte[] transformedKeyDigest; UUID kdf = null; if (kdfparameters != null) { @@ -134,19 +153,13 @@ public InputStream createDecryptedStream(byte[] digest, InputStream inputStream) } // v3 doesn't have a kdf therefore AES if (kdf == null || Aes.KDF.equals(kdf)){ - finalKeyDigest = Aes.getFinalKeyDigest(digest, getMasterSeed(), getTransformSeed(), getTransformRounds()); + transformedKeyDigest = Aes.getTransformedKey(digest, getTransformSeed(), getTransformRounds()); } else if (Argon.argon2_kdf.equals(kdf)) { - finalKeyDigest = Argon.getArgonFinalKeyDigest(digest, getMasterSeed(), kdfparameters); + transformedKeyDigest = Argon.getTransformedKey(digest, kdfparameters); } else { throw new UnsupportedOperationException("Unknown transform KDF " + kdf); } - - if (AES_CIPHER.equals(cipherUuid)) { - return Encryption.getDecryptedInputStream(inputStream, Aes.getCipher(), finalKeyDigest, getEncryptionIv()); - } else if (CHACHA_CIPHER.equals(cipherUuid)) { - return Encryption.getDecryptedInputStream(inputStream, ChaCha.getCipher(), finalKeyDigest, getEncryptionIv()); - } - throw new UnsupportedOperationException("Unknown encryption cipher " + cipherUuid); + return transformedKeyDigest; } /** @@ -158,7 +171,10 @@ public InputStream createDecryptedStream(byte[] digest, InputStream inputStream) * @throws IOException if something bad happens */ public OutputStream createEncryptedStream(byte[] digest, OutputStream outputStream) throws IOException { - byte[] finalKeyDigest = Aes.getFinalKeyDigest(digest, getMasterSeed(), getTransformSeed(), getTransformRounds()); + // return digest of master seed and hash + MessageDigest md = getSha256MessageDigestInstance(); + md.update(masterSeed); + byte[] finalKeyDigest = md.digest(getTransformedKeyDigest(digest)); return Encryption.getEncryptedOutputStream(outputStream, finalKeyDigest, getEncryptionIv()); } @@ -285,5 +301,13 @@ public void setCustomData(VariantDictionary customData) { public void addBinary(byte[] bytes) { } + public byte[] getHeaderBytes() { + return headerBytes; + } + public void setHeaderBytes(byte[] headerBytes) { + byte [] copy = new byte[headerBytes.length]; + System.arraycopy(headerBytes, 0, copy, 0, headerBytes.length); + this.headerBytes = copy; + } } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java index 94dfb5f5..ec980053 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java @@ -19,18 +19,20 @@ import com.google.common.io.LittleEndianDataInputStream; import com.google.common.io.LittleEndianDataOutputStream; import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.hashedblock.CollectingInputStream; import org.linguafranca.pwdb.hashedblock.HashedBlockInputStream; import org.linguafranca.pwdb.hashedblock.HashedBlockOutputStream; import org.linguafranca.pwdb.hashedblock.HmacBlockInputStream; import org.linguafranca.pwdb.security.Encryption; import org.linguafranca.pwdb.security.VariantDictionary; +import org.spongycastle.crypto.digests.SHA256Digest; +import org.spongycastle.crypto.macs.HMac; +import org.spongycastle.crypto.params.KeyParameter; import java.io.*; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.security.DigestInputStream; -import java.security.DigestOutputStream; -import java.security.MessageDigest; +import java.security.*; import java.util.Arrays; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; @@ -101,9 +103,12 @@ public static InputStream createUnencryptedInputStream(Credentials credentials, if (kdbxHeader.getVersion() >= 4) { - HmacBlockInputStream hmacBlockInputStream = new HmacBlockInputStream(inputStream, true); + byte[] hmacKey = verifyOuterHeader(kdbxHeader, credentials, new DataInputStream(inputStream)); + + HmacBlockInputStream hmacBlockInputStream = new HmacBlockInputStream(hmacKey, inputStream, true); plainTextStream = kdbxHeader.createDecryptedStream(credentials.getKey(), hmacBlockInputStream); + } else { InputStream decryptedInputStream = kdbxHeader.createDecryptedStream(credentials.getKey(), inputStream); @@ -188,8 +193,7 @@ private static class HeaderType { } /** - * Read two lots of 4 bytes and verify that they satisfy the signature of a - * kdbx file; + * Read two lots of 4 bytes and verify that they satisfy the signature of a kdbx file; * @param ledis an input stream * @return true if it looks like this is a kdbx file * @throws IOException on error @@ -210,10 +214,12 @@ public static KdbxHeader readOuterHeader(InputStream inputStream, KdbxHeader kdb // header is digested to verify correctness MessageDigest digest = Encryption.getSha256MessageDigestInstance(); - // we do not close this stream, otherwise we lose our place in the underlying stream DigestInputStream shaDigestInputStream = new DigestInputStream(inputStream, digest); - // we do not close this stream, otherwise we lose our place in the underlying stream - LittleEndianDataInputStream ledis = new LittleEndianDataInputStream(shaDigestInputStream); + // collect the bytes of the header, we'll need them later + CollectingInputStream collectingInputStream = new CollectingInputStream(shaDigestInputStream, true); + // make values available from LittleEndian + LittleEndianDataInputStream ledis = new LittleEndianDataInputStream(collectingInputStream); + // file starts with magic number if (!verifyMagicNumber(ledis)) { throw new IllegalStateException("Magic number did not match"); @@ -225,10 +231,9 @@ public static KdbxHeader readOuterHeader(InputStream inputStream, KdbxHeader kdb // read header fields getOuterHeaderFields(kdbxHeader, digest, ledis); - if (kdbxHeader.getVersion() > 3) { - // v4 contains header hashes - verifyOuterHeader(kdbxHeader, ledis); - } + // stop collecting the bytes of the header + collectingInputStream.setCollecting(false); + kdbxHeader.setHeaderBytes(collectingInputStream.getCollectedBytes()); return kdbxHeader; } @@ -236,16 +241,43 @@ public static KdbxHeader readOuterHeader(InputStream inputStream, KdbxHeader kdb /** * V4 header is followed by an SHA256 and then contains an HMACSHA256 after that. * @param kdbxHeader the header containing the relevant parameters + * @param credentials * @param input an input source + * @return Hmac Key Digest for use in the HmacBlockInputStream * @throws IOException on error */ - private static void verifyOuterHeader(KdbxHeader kdbxHeader, DataInput input) throws IOException { + public static byte[] verifyOuterHeader(KdbxHeader kdbxHeader, Credentials credentials, DataInput input) throws IOException { + // check the SHA byte [] sha256 = getBytes(32, input); if (!Arrays.equals(kdbxHeader.getHeaderHash(), sha256)) { throw new IllegalStateException("Header hash does not match"); } - byte [] hmacSha256 = getBytes(32, input); - // TODO verify HMAC + + // Compute the Hmac Key Digest + // KdbxFile.cs Computekeys + MessageDigest md = Encryption.getSha512MessageDigestInstance(); + md.update(kdbxHeader.getMasterSeed()); + md.update(kdbxHeader.getTransformedKeyDigest(credentials.getKey())); + byte [] hmacKey = md.digest(new byte [] {1}); + + // get the key for the header Hmac (using sequence number -1) + // KdbxFile.cs ComputeHeaderHmac + byte [] hmacKey64 = HmacBlockInputStream.getHmacBlockKey(hmacKey, -1L, ByteOrder.LITTLE_ENDIAN); + + HMac hmac = new HMac(new SHA256Digest()); + hmac.init(new KeyParameter(hmacKey64)); + hmac.update(kdbxHeader.getHeaderBytes(), 0, kdbxHeader.getHeaderBytes().length); + byte[] computedHmacSha256 = new byte[32]; + hmac.doFinal(computedHmacSha256, 0); + + // check for correctness + byte [] storedHmacSha256 = getBytes(32, input); + + if (!Arrays.equals(computedHmacSha256, storedHmacSha256)) { + throw new IllegalStateException("Header HMAC does not match"); + } + return hmacKey; + } private static void getOuterHeaderFields(KdbxHeader kdbxHeader, MessageDigest digest, DataInput input) throws IOException { diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java index 3e75670d..3c9adccd 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java @@ -1,8 +1,10 @@ package org.linguafranca.pwdb.kdbx.stream; +import com.google.common.io.LittleEndianDataInputStream; import org.junit.Ignore; import org.junit.Test; import org.linguafranca.pwdb.hashedblock.HmacBlockInputStream; +import org.linguafranca.pwdb.kdbx.KdbxCredentials; import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.kdbx.KdbxHeader; import org.linguafranca.pwdb.kdbx.KdbxSerializer; @@ -28,7 +30,8 @@ public void getHmacStream() throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); System.out.println("Version " + header.getVersion()); - HmacBlockInputStream hmacBlockInputStream = new HmacBlockInputStream(inputStream, true); + byte [] hmacKey = KdbxSerializer.verifyOuterHeader(header, new KdbxCredentials.Password("123".getBytes()), new LittleEndianDataInputStream(inputStream)); + HmacBlockInputStream hmacBlockInputStream = new HmacBlockInputStream(hmacKey, inputStream, true); byte [] buf = new byte [1024]; int bytesRead; while ((bytesRead = hmacBlockInputStream.read(buf)) != -1) { From 3006aecd309e29ccea80af11ef713bcfd3e4cee4 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Thu, 1 Feb 2018 13:43:18 +0000 Subject: [PATCH 060/228] Changes to simple for V4 XML etc. --- .../org/linguafranca/pwdb/security/Aes.java | 2 +- kdb/pom.xml | 1 - .../linguafranca/pwdb/kdb/KdbCredentials.java | 4 +++- .../org/linguafranca/pwdb/kdbx/Helpers.java | 20 ++++++++++++++++--- .../linguafranca/pwdb/kdbx/KdbxHeader.java | 6 ++++++ .../pwdb/kdbx/stream/KdbxSerializerTest.java | 10 ++++++---- .../pwdb/kdbx/simple/SimpleDatabase.java | 8 +++++--- .../pwdb/kdbx/simple/model/KeePassFile.java | 6 ++++++ .../kdbx/simple/SimpleDatabaseLoadTest.java | 15 ++++++++++++++ 9 files changed, 59 insertions(+), 13 deletions(-) diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java index 3f5a7f60..5d65c0ad 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java @@ -11,7 +11,7 @@ import static org.linguafranca.pwdb.security.Encryption.getSha256MessageDigestInstance; /** - * AES may be used for Key Derivation and aslo as the underlying stream cipher + * AES may be used for Key Derivation and also as the underlying stream cipher * * @author jo */ diff --git a/kdb/pom.xml b/kdb/pom.xml index df4b4533..921eb6d4 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -35,7 +35,6 @@ org.apache.maven.plugins maven-jar-plugin - ${maven-jar-plugin.version} diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java index 94982d88..9d40d54d 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java @@ -66,8 +66,10 @@ public KeyFile(byte[] password, InputStream inputStream) { byte [] keyFileData = ByteStreams.toByteArray(inputStream); if (keyFileData.length == 64) { keyFileData = Hex.decode(keyFileData); + key = md.digest(keyFileData); + return; } - key = md.digest(keyFileData); + throw new IllegalStateException("Invalid length key file " + keyFileData.length); } catch (IOException e) { throw new IllegalStateException("Could not read key file", e); } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java index d8225c2a..4597a534 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java @@ -24,6 +24,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; @@ -76,12 +77,25 @@ public static String fromBoolean(Boolean value) { private static SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + private static Date baseDate; + + static { + try { + baseDate = inFormat.parse("0001-01-01T00:00:00Z"); + } catch (ParseException ignore) { + // hmm, cannot happen + } + } + + // in V3 this is just a date, in V4 it's a base64 encoded serial number of seconds after the base date above public static Date toDate(String value) { try { return inFormat.parse(value); - } catch (ParseException e) { - throw new IllegalStateException(e); - } + } catch (ParseException ignored) {} + // V4 + byte [] b = decodeBase64Content(value.getBytes()); + long secondsSinceBaseDate = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getLong(); + return new Date(secondsSinceBaseDate * 1000 + baseDate.getTime()); } public static String fromDate(Date value) { diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java index 5956036f..ff594e3e 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java @@ -144,6 +144,12 @@ public InputStream createDecryptedStream(byte[] digest, InputStream inputStream) throw new UnsupportedOperationException("Unknown encryption cipher " + cipherUuid); } + public StreamEncryptor getInnerStreamEncryptor () { + return getVersion() == 4 ? + new StreamEncryptor.ChaCha20(getInnerRandomStreamKey()) : + new StreamEncryptor.Salsa20(getInnerRandomStreamKey()); + } + public byte[] getTransformedKeyDigest(byte[] digest) { byte[] transformedKeyDigest; diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java index 3c9adccd..ec33fe6e 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java @@ -9,8 +9,7 @@ import org.linguafranca.pwdb.kdbx.KdbxHeader; import org.linguafranca.pwdb.kdbx.KdbxSerializer; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; import java.util.Scanner; /** @@ -53,15 +52,18 @@ public void loadChaChaAesHeader() throws IOException { System.out.println("Version " + header.getVersion()); } - @Test @Ignore + @Test public void getDecryptedArgonInputStream() throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); InputStream is = KdbxSerializer.createUnencryptedInputStream(new KdbxCreds("123".getBytes()), new KdbxHeader(), inputStream); + + PrintWriter pw = new PrintWriter(new FileOutputStream("temp.xml")); Scanner s = new Scanner(is); while (s.hasNext()) { - System.out.println(s.nextLine()); + pw.println(s.nextLine()); } s.close(); + pw.close(); } @Test diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java index b92eb2ab..3d519cf0 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java @@ -184,13 +184,15 @@ public static SimpleDatabase load(Credentials credentials, InputStream inputStre KdbxHeader kdbxHeader = new KdbxHeader(); InputStream kdbxInnerStream = KdbxSerializer.createUnencryptedInputStream(credentials, kdbxHeader, inputStream); + StreamEncryptor streamEncyptor = kdbxHeader.getInnerStreamEncryptor(); + // decrypt the encrypted fields in the inner XML stream - InputStream plainTextXmlStream = new XmlInputStreamFilter(kdbxInnerStream, - new KdbxInputTransformer(new StreamEncryptor.Salsa20(kdbxHeader.getInnerRandomStreamKey()))); + InputStream plainTextXmlStream = new XmlInputStreamFilter(kdbxInnerStream, new KdbxInputTransformer(streamEncyptor)); // read the now entirely decrypted stream into database KeePassFile result = getSerializer().read(KeePassFile.class, plainTextXmlStream); - if (!Arrays.equals(result.meta.headerHash.getContent(), kdbxHeader.getHeaderHash())) { + + if (kdbxHeader.getVersion() == 3 && !Arrays.equals(result.meta.headerHash.getContent(), kdbxHeader.getHeaderHash())) { throw new IllegalStateException("Header Hash Mismatch"); } diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java index 9668d5e9..8eb46380 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java @@ -123,6 +123,12 @@ public static class Meta { protected List binaries; @Element(name = "CustomData", required = false) protected KeePassFile.CustomData customData; + + /* version 4 */ + + @Element(name = "SettingsChanged", required = false, type = Date.class) + @Convert(TimeConverter.class) + protected Date settingsChanged; } diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java index 8e06bbad..8c42a892 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java @@ -17,6 +17,8 @@ package org.linguafranca.pwdb.kdbx.simple; import org.junit.Test; +import org.linguafranca.pwdb.Entry; +import org.linguafranca.pwdb.Group; import org.linguafranca.pwdb.Visitor; import org.linguafranca.pwdb.kdbx.KdbxCreds; @@ -39,6 +41,19 @@ public void loadKdbx() throws Exception { SimpleDatabase database = SimpleDatabase.load(new KdbxCreds("123".getBytes()), inputStream); database.visit(new Visitor.Print()); } + @Test + public void loadKdbxV4() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); + SimpleDatabase database = SimpleDatabase.load(new KdbxCreds("123".getBytes()), inputStream); + database.visit(new Visitor.Print()); + // test what happens to dates in V4 + database.visit(new Visitor.Default(){ + @Override + public void visit(Entry entry) { + System.out.println(entry.getCreationTime()); + } + }); + } @Test public void emptyDb() throws Exception { From 1757071e97a5cd849702df73915bff551fac444e Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 3 Feb 2018 16:01:23 +0000 Subject: [PATCH 061/228] Adding V4 date and header compatibility to DomWrapper and Jaxb. --- CHANGELOG.md | 1 + .../kdbx/dom/DomSerializableDatabase.java | 25 +++++++++++++++- .../linguafranca/pwdb/kdbx/dom/DomV4Test.java | 28 ++++++++++++++++++ .../pwdb/kdbx/jaxb/JaxbV4LoadTest.java | 28 ++++++++++++++++++ .../linguafranca/pwdb/kdbx/KdbxHeader.java | 5 +++- .../pwdb/kdbx/KdbxStreamFormat.java | 29 +++++++++++++++++-- 6 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomV4Test.java create mode 100644 jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbV4LoadTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 46cfbdae..59673e23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.co ### Added - Preliminary readonly support for V4 files +- updated documentation of various sorts ##[2.1.4-Snapshot] diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java index b2fa7993..d50933bc 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java @@ -17,6 +17,7 @@ package org.linguafranca.pwdb.kdbx.dom; +import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.SerializableDatabase; import org.linguafranca.pwdb.kdbx.StreamEncryptor; import org.apache.commons.codec.binary.Base64; @@ -38,6 +39,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.security.SecureRandom; +import java.text.ParseException; import java.util.Date; /** @@ -62,12 +64,14 @@ public static DomSerializableDatabase createEmptyDatabase() throws IOException { // read in the template KeePass XML database result.load(result.getClass().getClassLoader().getResourceAsStream("base.kdbx.xml")); try { - // replace all placeholder dates with now + // replace all placeholder dates with now (this is now already done in the loader) +/* String now = DomHelper.dateFormatter.format(new Date()); NodeList list = (NodeList) DomHelper.xpath.evaluate("//*[contains(text(),'${creationDate}')]", result.doc.getDocumentElement(), XPathConstants.NODESET); for (int i = 0; i < list.getLength(); i++) { list.item(i).setTextContent(now); } +*/ // set the root group UUID Node uuid = (Node) DomHelper.xpath.evaluate("//"+ DomHelper.UUID_ELEMENT_NAME, result.doc.getDocumentElement(), XPathConstants.NODE); uuid.setTextContent(DomHelper.base64RandomUuid()); @@ -97,7 +101,16 @@ public SerializableDatabase load(InputStream inputStream) throws IOException { element.removeAttribute("Protected"); } + // we need to convert all V4 dates - we'll convert them all anyway TODO find a way of not converting V3 dates + // finding all elements name ending Changed and Time + NodeList dateContent = (NodeList) DomHelper.xpath.evaluate("//*[substring(local-name(), string-length(local-name()) -3) = 'Time']", doc, XPathConstants.NODESET); + processDates(dateContent); + + dateContent = (NodeList) DomHelper.xpath.evaluate("//*[substring(local-name(), string-length(local-name()) -6) = 'Changed']", doc, XPathConstants.NODESET); + processDates(dateContent); + return this; + } catch (ParserConfigurationException e) { throw new IllegalStateException("Instantiating Document Builder", e); } catch (SAXException e) { @@ -107,6 +120,16 @@ public SerializableDatabase load(InputStream inputStream) throws IOException { } } + private void processDates(NodeList dateContent) { + Date now = new Date(); + for (int i = 0; i < dateContent.getLength(); i++){ + Element element = ((Element) dateContent.item(i)); + String content = DomHelper.getElementContent(".", element); + Date d = content == null || content.equals("${creationDate}") ? now :Helpers.toDate(content); + DomHelper.setElementContent(".", element, DomHelper.dateFormatter.format(d)); + } + } + @Override public void save(OutputStream outputStream) { Document copyDoc = (Document) doc.cloneNode(true); diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomV4Test.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomV4Test.java new file mode 100644 index 00000000..ae78365d --- /dev/null +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomV4Test.java @@ -0,0 +1,28 @@ +package org.linguafranca.pwdb.kdbx.dom; + +import org.junit.Test; +import org.linguafranca.pwdb.Entry; +import org.linguafranca.pwdb.Visitor; +import org.linguafranca.pwdb.kdbx.KdbxCreds; + +import java.io.InputStream; + +/** + * @author jo + */ +public class DomV4Test { + @Test + public void loadKdbxV4() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); + DomDatabaseWrapper database = DomDatabaseWrapper.load(new KdbxCreds("123".getBytes()), inputStream); + database.visit(new Visitor.Print()); + // test what happens to dates in V4 + database.visit(new Visitor.Default(){ + @Override + public void visit(Entry entry) { + System.out.println(entry.getCreationTime()); + } + }); + } + +} diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbV4LoadTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbV4LoadTest.java new file mode 100644 index 00000000..7d076029 --- /dev/null +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbV4LoadTest.java @@ -0,0 +1,28 @@ +package org.linguafranca.pwdb.kdbx.jaxb; + +import org.junit.Test; +import org.linguafranca.pwdb.Entry; +import org.linguafranca.pwdb.Visitor; +import org.linguafranca.pwdb.kdbx.KdbxCreds; + +import java.io.InputStream; + +/** + * @author jo + */ +public class JaxbV4LoadTest { + @Test + public void loadKdbxV4() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); + JaxbDatabase database = JaxbDatabase.load(new KdbxCreds("123".getBytes()), inputStream); + database.visit(new Visitor.Print()); + // test what happens to dates in V4 + database.visit(new Visitor.Default(){ + @Override + public void visit(Entry entry) { + System.out.println(entry.getCreationTime()); + } + }); + } + +} diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java index ff594e3e..044c7c18 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java @@ -108,6 +108,10 @@ public enum ProtectedStreamAlgorithm { * Construct a default KDBX header */ public KdbxHeader() { + this(3); + } + + public KdbxHeader(int version) { SecureRandom random = new SecureRandom(); cipherUuid = AES_CIPHER; compressionFlags = CompressionFlags.GZIP; @@ -118,7 +122,6 @@ public KdbxHeader() { innerRandomStreamKey = random.generateSeed(32); streamStartBytes = new byte[32]; protectedStreamAlgorithm = ProtectedStreamAlgorithm.SALSA_20; - version = 3; } /** diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java index d0dde97a..5691ae62 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.Arrays; /** * This class implements KDBX formatted saving and loading of databases @@ -29,21 +30,45 @@ */ public class KdbxStreamFormat implements StreamFormat { + private final Version version; + + public static enum Version {KDBX31, KDBX4} + + /** + * Create a StreamFormat for reading or for writing v3 + */ + public KdbxStreamFormat() { + this.version = Version.KDBX31; + } + + /** + * Specify a version for writing + * @param version the version + */ + public KdbxStreamFormat(Version version) { + this.version = version; + } + @Override public void load(SerializableDatabase serializableDatabase, Credentials credentials, InputStream encryptedInputStream) throws IOException { KdbxHeader kdbxHeader = new KdbxHeader(); InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, kdbxHeader, encryptedInputStream); serializableDatabase.setEncryption(kdbxHeader.getStreamEncryptor()); serializableDatabase.load(decryptedInputStream); + if (kdbxHeader.getVersion() == 3 && !Arrays.equals(serializableDatabase.getHeaderHash(), kdbxHeader.getHeaderHash())) { + throw new IllegalStateException("Header hash does not match"); + } decryptedInputStream.close(); } @Override public void save(SerializableDatabase serializableDatabase, Credentials credentials, OutputStream encryptedOutputStream) throws IOException { // fresh kdbx header - KdbxHeader kdbxHeader = new KdbxHeader(); + KdbxHeader kdbxHeader = new KdbxHeader(4); OutputStream unencrytedOutputStream = KdbxSerializer.createEncryptedOutputStream(credentials, kdbxHeader, encryptedOutputStream); - serializableDatabase.setHeaderHash(kdbxHeader.getHeaderHash()); + if (version == Version.KDBX31) { + serializableDatabase.setHeaderHash(kdbxHeader.getHeaderHash()); + } serializableDatabase.setEncryption(kdbxHeader.getStreamEncryptor()); serializableDatabase.save(unencrytedOutputStream); unencrytedOutputStream.flush(); From 8093932b1f717a30f8093c11e06db4532f82a699 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 3 Feb 2018 16:01:55 +0000 Subject: [PATCH 062/228] Added a V4 KDBX xsd --- KDBX.4.xsd | 542 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 542 insertions(+) create mode 100644 KDBX.4.xsd diff --git a/KDBX.4.xsd b/KDBX.4.xsd new file mode 100644 index 00000000..aca0cdef --- /dev/null +++ b/KDBX.4.xsd @@ -0,0 +1,542 @@ + + + + + This is an attempt to document the KDBX 3.1 and 4 XML formats as used by + KeePass 2.x compatible programs. + + Conventions in this schema: + + KeePassFile, Group, Entry, Times and History are top level elements as they may be useful for + creating fragments or something. + + There are types for almost everything that contains documentation as to its purpose + and how it's used and if there are restrictions on the values. + + Everything else is simply included inline for the sake of brevity. Undoubtedly + more documentation could most usefully be added. + + Many elements are probably optional but are not marked as such. In real life they are serialised + as empty elements and some are serialised with content "null". You have been warned. + + + + + + + + The root element. doh. + + + + + + + This contains database settings and also contains information + about the last state of the program that used the database. Presumably assuming + that the same program will be used to open it again, or that a different program + would want/need the same settings. + + + + + + + Header hash removed from version 4. Hashes now contained + in the kdbx header itself. + + + + + + + + + + + + + + + + + When the XML database is loaded into memory, sensitive + fields will be encrypted in that stream. This element specifies which of the + standardFields are encrypted in that stream, which is decrypted using Salsa20 + and the InnerRandomStream encryption header. + + /./Entry/String/Value/@Protected signifies that the value of a stringField is + so encrypted and applies also to customFields. + + + + + + + + + + + + + + + + + + + + + + + + + Used to store binary values in version 3.1 - in version 4 + such values are now stored as part of the KDBX inner headers. + + + + + + + + + + + + + + This is the root group which contains everything. + + + + + + When the user permanently deletes an wrappedEntry or a group, + an item is created in this node. This allows the synchronization function + to decide whether an object has been deleted or is just missing in one of + the databases that are being synchronized. + + + + + + + + + + + + + + A parent type for Entry and Group that allows us to + insert a superclass holding a link to a parent group during automatic + class generation. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + When it was last updated, resulting in an update to History if that is + active. + + + + + + When it was originally created. + + + + + When was last used (read). + + + + + When it expires if it expires. + + + + + True if it expires. + + + + + How often the object has been used (e.g. by copying data of it to the + clipboard, performing auto-type with it, etc.). However, a change to the usage count is + not considered to be a database change (i.e. the database is not marked as changed by the + UI), thus this value is typically incorrect (and the same applies to the last access + time). + + + + + + The date/time when the object was last moved. This allows the + synchronization function to correctly determine the most recent location of the + object. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This is ISO Date time stored as UTC (Z time zone). Left + to its own devices JAXB will make a XMLGregorianCalendar for these fields + which we don't want. + + In KDBX Version 4 this becomes a base64 encoded offset in seconds + from 0001-01-01T00:00:00Z. + + + + + + + + XSD doesn't do comma separated lists so we can't model this handily. + + Assume that the format is something like [{token}{, token}*]+ where white space is ignored except + as part of the name of the token. + + + + + + + + + This is where the values of the database are actually stored. You can have String valued + fields and you can have Binary valued fields. There are "Default" String fields (username and so on) and + there are custom string fields (custom only in that their names are not the names of default string + fields. Not really clear whether the keys are case sensitive. + + + + + + + + + + + + + + + + + + + + If derived from a file then the Key element conventionally contains the file name. + I think it would be useful to add a mime type on this ... + + The Value element is the value of /./meta/binaries/binary/@ID that is referenced from this + binary. + + + + + + + + + + + + + + + + + An index to one of the 69 built in icons. + + + + + + + + + + + + Simply put: "#rrggbb". + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Specifies the override URL, as documented at + http://keepass.info/help/base/autourl.html#override + + + + + + + + + + Stores everything related to auto-type for this wrappedEntry. + http://keepass.info/help/base/autotype.html + + + + + + + 1 if Two-Channel Auto-Type Obfuscation is enabled, otherwise 0. + http://keepass.info/help/v2/autotype_obfuscation.html + + + + + + + + + + + + + Custom window/sequence association + + + + + + + + + + + + + + + + Number of days before triggering an event e.g. suggesting a password change. + -1 means never. 0 means every time. + + + + + + + + + + + + A base64 encoded UUID. Each Group and Entry has a UUID. When added to History the same + UUID is maintained. + + + + + + + + A reference to the UUID e.g. a Group like the Recycle bin. If, for + example, the recycle bin is enabled but has not been used, then it won't have been + created and the value stored here is a 0 UUID. + + + + + + + + A reference to the custom icon UUID found in the meta/customIcons list. + + + + + + + + + Binary field for elements are centrally stored in this element. + The same Binary element may be referenced from many Entries. + + + + + + The ID attribute is referenced in the Ref attribute of + a corresponding Binary element of an Entry. The Compressed attribute specifies + whether the base64 encoded value is to be decompressed to recover the binary + data. + + + + + + + + + + + + + + + + + + + + Third party programs and plugins can put custom data here. + Unique element names should be used, e.g. "PluginName_ItemName". + + + + + + + + \ No newline at end of file From ee3bb5d427147d2eaa7fd3f967d694ea2ed586b1 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 3 Feb 2018 16:19:55 +0000 Subject: [PATCH 063/228] [maven-release-plugin] prepare release KeePassJava2-2.1.4 --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- http/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 4 ++-- simple/pom.xml | 2 +- test/pom.xml | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index 3ac4774c..97af2fdd 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.1.4 ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index 129571ea..a7b3523a 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.1.4 ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index f35731a2..bb860e78 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.1.4 ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index f0a94c94..e9e715aa 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.1.4 ../pom.xml 4.0.0 diff --git a/http/pom.xml b/http/pom.xml index a6fff8d3..568d2543 100644 --- a/http/pom.xml +++ b/http/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.1.4 ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 3acc7338..c3b9bb89 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.1.4 ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index b1ce06f1..7ebe28f5 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.1.4 ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index ccab40c3..7cc5d57e 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.1.4 ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index fbca0744..7ff9afd9 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.1.4-SNAPSHOT + 2.1.4 database test @@ -51,7 +51,7 @@ scm:git:git://github.com/jorabin/KeePassJava2.git scm:git:git@github.com:jorabin/KeePassJava2.git https://github.com/jorabin/KeePassJava2 - HEAD + KeePassJava2-2.1.4 diff --git a/simple/pom.xml b/simple/pom.xml index c17da12c..71c28b3b 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.1.4 ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index 63bc46cb..ed37aa02 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4-SNAPSHOT + 2.1.4 4.0.0 From 43d5daa9cb7e069d5311c8cdebe63e01124878d4 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 3 Feb 2018 16:20:01 +0000 Subject: [PATCH 064/228] [maven-release-plugin] prepare for next development iteration --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- http/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 4 ++-- simple/pom.xml | 2 +- test/pom.xml | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index 97af2fdd..88f4e17e 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4 + 2.1.5-SNAPSHOT ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index a7b3523a..5eb5e714 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4 + 2.1.5-SNAPSHOT ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index bb860e78..291e6119 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4 + 2.1.5-SNAPSHOT ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index e9e715aa..6a563efe 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4 + 2.1.5-SNAPSHOT ../pom.xml 4.0.0 diff --git a/http/pom.xml b/http/pom.xml index 568d2543..ae3e0d32 100644 --- a/http/pom.xml +++ b/http/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4 + 2.1.5-SNAPSHOT ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index c3b9bb89..36939cf0 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4 + 2.1.5-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index 7ebe28f5..78645793 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4 + 2.1.5-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index 7cc5d57e..56332e4e 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4 + 2.1.5-SNAPSHOT ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index 7ff9afd9..d19e277d 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.1.4 + 2.1.5-SNAPSHOT database test @@ -51,7 +51,7 @@ scm:git:git://github.com/jorabin/KeePassJava2.git scm:git:git@github.com:jorabin/KeePassJava2.git https://github.com/jorabin/KeePassJava2 - KeePassJava2-2.1.4 + HEAD diff --git a/simple/pom.xml b/simple/pom.xml index 71c28b3b..1ed10cd9 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4 + 2.1.5-SNAPSHOT ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index ed37aa02..31c3e23d 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.4 + 2.1.5-SNAPSHOT 4.0.0 From 6c051118651fd82028d2286e4d8cac0446c14fe6 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 3 Feb 2018 16:26:39 +0000 Subject: [PATCH 065/228] UPdate changelog with 2.1.4 release date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c78f106b..4c9b729d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.com) and [Semantic Versioning](http://semver.org/spec/v2.0.0.html) -##[2.1.4-Snapshot] +##[2.1.4] 2018-02-03 ### Added From c0f9af7a0a0a41b784cabc0781f8ec1a85ddd251 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 3 Feb 2018 17:12:29 +0000 Subject: [PATCH 066/228] Initial Bitbucket Pipelines configuration --- bitbucket-pipelines.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 bitbucket-pipelines.yml diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml new file mode 100644 index 00000000..d17bf5af --- /dev/null +++ b/bitbucket-pipelines.yml @@ -0,0 +1,14 @@ +# This is a sample build configuration for Java (Maven). +# Check our guides at https://confluence.atlassian.com/x/zd-5Mw for more examples. +# Only use spaces to indent your .yml configuration. +# ----- +# You can specify a custom docker image from Docker Hub as your build environment. +image: maven:3.5.2-jdk-7 + +pipelines: + default: + - step: + caches: + - maven + script: # Modify the commands below to build your repository. + - mvn -B verify # -B batch mode makes Maven less verbose \ No newline at end of file From 4fd0c4f6c3ef156c8323f1295c8f55339a9e2988 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 3 Feb 2018 17:37:50 +0000 Subject: [PATCH 067/228] Documentation Update --- Format Diagram.svg => FormatDiagram.svg | 0 Module Structure.svg => ModuleStructure.svg | 0 readme.md | 6 +++--- 3 files changed, 3 insertions(+), 3 deletions(-) rename Format Diagram.svg => FormatDiagram.svg (100%) rename Module Structure.svg => ModuleStructure.svg (100%) diff --git a/Format Diagram.svg b/FormatDiagram.svg similarity index 100% rename from Format Diagram.svg rename to FormatDiagram.svg diff --git a/Module Structure.svg b/ModuleStructure.svg similarity index 100% rename from Module Structure.svg rename to ModuleStructure.svg diff --git a/readme.md b/readme.md index 115d5fc2..65eb31b8 100644 --- a/readme.md +++ b/readme.md @@ -35,7 +35,7 @@ The composite POM is org.linguafranca.pwdb KeePassJava2 - 2.1.3 + 2.1.4 at Maven Central. Note that the artifactId has become Camel Case from release 2.1.x onwards. @@ -126,7 +126,7 @@ clarification and my own satisfaction I have written about my understanding of K 1. The Javadoc header to [KdbxSerializer](kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.java) describes KDBX stream formatting. 2. The XSD Schema [KDBX.3.1.xsd](KDBX.3.1.xsd) documents my understanding of the Keepass XML, and also my lack of understanding, in parts. -3. [This graphic](Format Diagram.svg) illustrates KDBX 3.1 stream format and also illustrates proposals for the revised KDBX 4.0 format. +3. [![KDBX Formats](./FormatDiagram.svg "KDBX Formats")](./FormatDiagram.svg) ## Dependencies @@ -151,7 +151,7 @@ Included POM is for Maven 3. There are rather a lot of modules, this is in order to allow loading of minimal necessary functionality. The module dependencies are illustrated below. -[![Module Structure](./Module Structure.svg "Module Structure")](./Module Structure.svg) +[![Module Structure](./ModuleStructure.svg "Module Structure")](./ModuleStructure.svg) Each module corresponds to a Maven artifact. The GroupId is `org.linguafranca.pwdb`. The version id is as noted [above](#mvn). From 584c9bffb70bc095f80f70975ed475a5d5bf6aee Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 3 Feb 2018 18:17:44 +0000 Subject: [PATCH 068/228] Inhibit auto bitbucket builds Update Changelog formatting --- CHANGELOG.md | 16 ++++++++-------- bitbucket-pipelines.yml | 3 ++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c9b729d..abf86b04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ -#Change Log +# Change Log Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.com) and [Semantic Versioning](http://semver.org/spec/v2.0.0.html) -##[2.1.4] 2018-02-03 +## [2.1.4] 2018-02-03 ### Added @@ -11,7 +11,7 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.co - expires functionality on Entry via @AugustNagro - database reports support for optional features -##[2.1.3] 2018-01-21 +## [2.1.3] 2018-01-21 ### Fixed @@ -24,13 +24,13 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.co - An experimental implementation of [keepasshttp](https://github.com/pfn/keepasshttp/) see [the readme](http/readme.md) for warnings, limitations, etc. about this. -##[2.1.2] 2018-01-20 +## [2.1.2] 2018-01-20 ### Fixed - [Issue #16] Fix for split package -##[2.1.1] 2017-01-27 +## [2.1.1] 2017-01-27 ### Fixed @@ -42,7 +42,7 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.co - Kdb Key File Support -##[2.1.0] 2016-10-29 +## [2.1.0] 2016-10-29 ### Added - Added a module structure to allow selective building, for android etc. @@ -54,7 +54,7 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.co - artifactId became Camel Case KeePassJava2 - Documentation - beefed up the README -##[2.0.1] 2016-10-02 +## [2.0.1] 2016-10-02 ### Added - this changelog file @@ -74,7 +74,7 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.co - KdbxCreds Introduced -##[2.0.0] 2016-08-31 +## [2.0.0] 2016-08-31 Starting at release 2.0.0 Since this is keepassjava2. Don't ask what happened to keepassjava1. diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index d17bf5af..a8c1de44 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -6,7 +6,8 @@ image: maven:3.5.2-jdk-7 pipelines: - default: + #only on this branch + bitbucket-build: - step: caches: - maven From 841c80fa586ab65296749ac9af62f2f23cc83e5b Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 5 Feb 2018 11:51:12 +0000 Subject: [PATCH 069/228] Update documentation and update dependencies --- HttpDatabase.kdbx | Bin 1582 -> 0 bytes Module Structure.xml | 1 - pom.xml | 31 ++++++++++++++------- readme.md | 64 ++++++++++++++++++++++++++++++++----------- 4 files changed, 69 insertions(+), 27 deletions(-) delete mode 100644 HttpDatabase.kdbx delete mode 100644 Module Structure.xml diff --git a/HttpDatabase.kdbx b/HttpDatabase.kdbx deleted file mode 100644 index 243bb55dcf10f7c0436270738adfc1d5babb123b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1582 zcmV+}2GRKg*`k_f`%AR}00RI55CAd3^5(yBLr}h01tDtuTK@wC0096100bZaBs_hS zZ%^VurSLN6i;(sX)Mt`p(GffoDQ_hq0irNl1t0(()IFjYFtz!9gQrKdCe6sB=lr%R z%~_QVbQ+g!ZiOrc2mo*w00000000LN0B9k_OK<=raF7WrdLnR2_OKU?m(@^>6XFEt{@RliH~}pH+eSy!dn7% zb#l9JJ?3Q!1ONg60000401XNa3JLqq%Mg|{4@S)fVHOf4OEKB$hc8FTaJPfU43~6W zi>Mv)sz{`5fMw>mGWF|fli9CgzUMl7!t(5p=ND*s(Q zXUw|Xj%=ZZIu!1|;0yQ?TAPdPKQpUb)$~A*;=F(7E@o%KPGT+lxMQkF+p;xtA?2_NrcS`+?X7Qs94u3dvl)r%7%QoSsJ(6bofgLqsg zs=#Zs2YXm<;-t-dy41E};FrO~;z8R;4CDupeI3G@G2NeE-e54|w9TN4nf}da+u4H0TKl zCtaKcA>2^=CO*CE@feE93Ld^TmxAVSNspWEQgU4PYw(i)>NpjC+L+%!C5@&*DI3vm z64;r$Qm!Jr4GrJVX$iOmx4QE9^V_kC_4|V`9CE)_~+O=LwD5znjihOHi1diXdcNs96=VT8Sd{~)8 zV`|?}8B;1fIc8ZSN2YxKW3}C|ji{cxAs1z}<=Gal5zPN41*FYv(sgx;G5iMJE8YIX z;v$*X`;lc_4(iq|V7LVYGg^Vwin<@4)g@}Z_uaR<9(!dPhIY_372jSP*)aU_itz)@d+%R=;r541>}F{dzN! zy(9MU2Yx*a`m^gv+Vpx+_|7Xs-+1l+u`hip+yEB=<8>JfYL_WYj`o~#c7eedBjF*C zr5;0H_E4g7V);mu4Fd>&X(4wKyA$VXp)Wu$F5);#!kb)WI#_V8d_f>H`;Y2xep&}6 z*6P5oA1Uyn^#oy-AWu0!U9;27^${9XaBT#H*vb&HD7Kzro9cn9>0z10s$n`xqZn%P`nQ85x&hXAc^(lK zMmZ5+E(C61Ye5G1?>`$a0KH--lPqeBQd*v9eieqFOdD1z$LnQ|&AagX&hz<&>O*1< z^i~UQ%9i3o4FD6}C!)4ywi*S;giWPrK?6Io+C2{$AT9eK{J-1WeUoH-x0VA+Y&KI^ g2Pc*rwT4jD`Wp7Vttb6M4EP41+VoBBkM+bl/2bnV3upV60u1+NNghbAmOHKdJ79efHQwEmzRpY6CJmlQVjG1snhk/Mx47E3C32P7G0HL+F8Ukn3gO3k7A/cTzIheK/1LwUgoC3ykFKctwKXIbwWP2H1HCqto6w2TVqsgpzXm2bAsTWhQk4S0ZYoxu2tVmNG/3ukQpMQSPCcpN6b8Z5nP1Wh5s5L+TLJ1XPbtwWpbEKHlKGV0Xqr+JB2a7T1m8QNWz1Iuu5gjTzZ4IPEzAHaOUl1eL7R3JJbQVbGW7rwdK63EzUvBTGnhR2eIZ5WtSDXk3MP5SgbGZZ5w8LlEi7zdC3xNwO+eLXNy54nKW5fkdzSnb1Zav6yWJkK84o09krwTDGAZQlKguCeNke3Dcbo2GMDJCF4SzF1FFNYChAlAZWG05m0ZdbmV08z1VeY4SImUiaf3sBiZxoZA6gFp4mai50+Oo+bA31FwDJILFtFO3lPE5TWmB8odGerubS0Q+wWkDSLYZ/yHFN4G6+1mVFGJge0Xy9qd6wC/C+YviG7TmVIiafv+kdKmeUaqhmvueqTAckAj7XQqLvBjARmHyFV9Xl0CErlmiailL4oilRNXyu5XKSI549tx++lkKCoZX0NTTVbS7+U5YJsZP2CXrDQ6lNzi83sL21LpxALgq3YGBdAecYXR3kUoIhppAhjtHslzTS4O6BOqt/j3BItbq8u/hNHakGlOGcCYAarVCwa7Mgu8HsO376+hoP2LyOnw/tOD6XdCPlYfvNXNdPVFCusOvOAr8wOmcGO8z83OJRTX9TjPRSa3dQIvsfF/TWjmtVKtGcV8YQy971ZaywupwP57WDwDa+uLIuLT64qIcQWNFNSanGVb0SZ992pXvafo70a7eoUpgkPAqWyyFwmzyMEYkmnVOdJhEJJ5183BIECSWeLheSlVrsEF52P/kYVXr3EXVuDwssfhQPBx+8vCeXbXD2PBMUzt1dvsGg/5C2/jK+FOPY4flz54yJBfIn+euzcblT+h+MP6cfvJnn3Y1YBwbGCyM6eLKSDjwxiThntKdF0jC52aYxyXhUNvE899Iwr5lEq43Za+IhF/bDDvD5toBbvShOPvNZuhrZqXvmR8Zl1b/bDOEhgN5wtcWxcNoTAfyuRF9wjwP7c/zkxXkDeThbWhoPO/foaGpDSY+Nxk1th83T+cIAt3aZdDjs66DQachdsKwnzwIGJJBzVNjGHEUo5XdbP0Jc6kDZeGmYmjpxJSOsj8kylMD5X/IittFOJDfzkhg9+lGON59LS0lwYgIez05mstLu7hOt5Ysx/Buf3kX7xrPGNhfvlWJT+tRw8luvu+9MPfI+g3qJ2HeWH/0XLFj+AWyRda3yk84ktx5ZAlPe3O+Q3oG0/ka6H7EI96aNx32XDzwDND+5nPCJp5o6szWfM1MGxXvytvAtQEqaEE0NJUI5VlaiNtEYCNPm95K5LIE5V9UwSLDeEfzXZqyALaWOAKRCXY31hagNo/L/HF/+0NIvkkmWIg3ENxNi9U14e3pGcEhATd31wXgBt7XBDfUHdmQcA90sO+yc3SghyTdu2IwVzvVNnYOHfS0n95LBvGo/ZywdLBnP6OkEF09CzByChF0/MIPfFV/NqPs8bditCBm0Ci76uu1KPuy0dX2uTrA7fq9pb5Sfhe2roHtfqhSxoYwl8EJzp7FZSov6UzSwVyE6VAsKYWgiFdlOFFWFb3u1e56AEtv8qxI12jGUJGgm+UGxzf3dXrY+SajmZlU4IFHXk8ApZFU7STtx0/itvnReMl3zQ/zwcP/ \ No newline at end of file diff --git a/pom.xml b/pom.xml index d19e277d..fa68c973 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,8 @@ ~ limitations under the License. --> - + 4.0.0 org.linguafranca.pwdb @@ -51,8 +52,8 @@ scm:git:git://github.com/jorabin/KeePassJava2.git scm:git:git@github.com:jorabin/KeePassJava2.git https://github.com/jorabin/KeePassJava2 - HEAD - + HEAD + @@ -65,6 +66,10 @@ + + 3.0.4 + + UTF-8 1.7 @@ -116,6 +121,11 @@ deploy + + org.apache.maven.plugins + maven-dependency-plugin + 3.0.2 + @@ -126,41 +136,42 @@ annotations 15.0 + com.google.guava guava - 19.0 + 24.0-android com.madgag.spongycastle core - 1.54.0.0 + 1.58.0.0 com.google.code.gson gson - 2.7 + 2.8.2 org.slf4j slf4j-api - 1.7.21 + 1.7.25 ch.qos.logback logback-core - 1.1.7 + 1.2.3 ch.qos.logback logback-classic - 1.1.7 + 1.2.3 commons-codec commons-codec - 1.10 + 1.11 junit diff --git a/readme.md b/readme.md index 65eb31b8..dfa544ce 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,8 @@ # KeePassJava2 [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent) -Master: [![Build Status](https://travis-ci.org/jorabin/KeePassJava2.svg?branch=master)](https://travis-ci.org/jorabin/KeePassJava2) Develop: [![Build Status](https://travis-ci.org/jorabin/KeePassJava2.svg?branch=develop)](https://travis-ci.org/jorabin/KeePassJava2) +Master: [![Build Status](https://travis-ci.org/jorabin/KeePassJava2.svg?branch=master)](https://travis-ci.org/jorabin/KeePassJava2) +Develop: [![Build Status](https://travis-ci.org/jorabin/KeePassJava2.svg?branch=develop)](https://travis-ci.org/jorabin/KeePassJava2) A Java 7 API for databases compatible with the renowned [KeePass](http://keepass.info) password safe for Windows. @@ -121,12 +122,13 @@ Hence the creation of abstract Database interfaces rather than following the Kee exactly. KeePass is in effect defined by the code that Dominik writes to create and maintain the project. -Hence there are no definitive specification of KeePass files other than that code. For the sake of +Hence there is no definitive specification of KeePass files other than that code. For the sake of clarification and my own satisfaction I have written about my understanding of KeePass formats in the following locations: -1. The Javadoc header to [KdbxSerializer](kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.java) describes KDBX stream formatting. +1. The Javadoc header to [KdbxSerializer](http://static.javadoc.io/org.linguafranca.pwdb/KeePassJava2-kdbx/2.1.4/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.html) describes version 3.1 KDBX stream formatting. 2. The XSD Schema [KDBX.3.1.xsd](KDBX.3.1.xsd) documents my understanding of the Keepass XML, and also my lack of understanding, in parts. -3. [![KDBX Formats](./FormatDiagram.svg "KDBX Formats")](./FormatDiagram.svg) +3. The following graphic illustrates KDBX 3.1 and 4 file formats: +[![KDBX Formats](./FormatDiagram.svg "KDBX Formats")](./FormatDiagram.svg) ## Dependencies @@ -134,7 +136,7 @@ Aside from the JRE the API depends on: - [Google Guava](https://github.com/google/guava/wiki) ([Apache 2 license](https://github.com/google/guava/blob/master/COPYING)). - [Apache Commons Codec](https://commons.apache.org/proper/commons-codec/) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0)). -- [Spongy Castle](https://rtyley.github.io/spongycastle/) which is a repackaging for Android of [Bouncy Castle](https://www.bouncycastle.org/java.html) ([Apache 2 license](https://www.bouncycastle.org/licence.html)). +- [Spongy Castle](https://rtyley.github.io/spongycastle/) ([License](https://github.com/rtyley/spongycastle/blob/spongy-master/LICENSE.html)) which is a repackaging for Android of [Bouncy Castle](https://www.bouncycastle.org/java.html) ([License](https://www.bouncycastle.org/licence.html)). The Simple XML implementation additionally depends on: @@ -157,21 +159,51 @@ Each module corresponds to a Maven artifact. The GroupId is `org.linguafranca.pw - + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + - + + + + + + +
    ModuleArtifactIdDescription
    ModuleArtifactIdJavaDocDescription
    databasedatabaseBase definition of the Database APIs.
    exampleexampleWorked examples of loading, saving, splicing etc. using the APIs
    testtestShared tests to assess the viability of the implementation.
    allKeePassJava2This is the main KeePassJava2 Maven dependency. Provides a route to all artifacts (other than test and examples) via transitive dependency.
    kdbKeePassJava2-kdbAn implementation of the Database APIs supporting KeePass KDB format.
    kdbxKeePassJava2-kdbxProvides support for KDBX streaming and security.
    simpleKeePassJava2-simpleA Simple XML Platform implementation of KDBX. Could be useful for Android.
    jaxbKeePassJava2-jaxbA JAXB implementation of KDBX. Probably not useful for Android. The generated class + +
    databasedatabase +Javadocs +Base definition of the Database APIs.
    exampleexampleJavadocsWorked examples of loading, saving, splicing etc. using the APIs
    testtestJavadocsShared tests to assess the viability of the implementation.
    allKeePassJava2(no JavaDoc)This is the main KeePassJava2 Maven dependency. Provides a route to all artifacts (other than test and examples) via transitive dependency.
    kdbKeePassJava2-kdbJavadocsAn implementation of the Database APIs supporting KeePass KDB format.
    kdbxKeePassJava2-kdbxJavadocsProvides support for KDBX streaming and security.
    simpleKeePassJava2-simpleJavadocsA Simple XML Platform implementation of KDBX. Could be useful for Android.
    jaxbKeePassJava2-jaxbJavadocsA JAXB implementation of KDBX. Probably not useful for Android. The generated class bindings might be useful for building other interfaces.
    domKeePassJava2-domA DOM based implementation of KDBX. Being DOM based it is rather slow, but + +
    domKeePassJava2-domJavadocsA DOM based implementation of KDBX. Being DOM based it is rather slow, but messes less with existing content than the other two implementations. Known to work on Android.
    httpkeepasshttpAn implementation of a server + +
    httpkeepasshttpAn implementation of a server intended to be the equivalent of keepasshttp, which is a plugin for Windows Keepass supporting communication with Chrome (chromeIPass) and Firefox(PassIFox) extensions. From b452cd54bea3ec1612bf688b268f57cdeffd061c Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 10 Feb 2018 13:32:07 +0000 Subject: [PATCH 070/228] Reverse merge of old format diagram --- FormatDiagram.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FormatDiagram.svg b/FormatDiagram.svg index 86b27bea..82baa74a 100644 --- a/FormatDiagram.svg +++ b/FormatDiagram.svg @@ -1,2 +1,2 @@ -
    Hashed Blocks
    Hashed Blocks
    Magic Number
    Magic Number
    Version
    Version
    Header
    Header
    Header
    Header
    Header
    Header
    Header 0
    Header 0
    ...
    ...
    Value of Header 9
    Value of Header 9
    0
    0
    8
    8
    4
    4
    Type
    Type
    Length
    Length
    Value
    Value
    Encrypted
    Encrypted
    Unencrypted
    Unencrypted
    Sequence
    Sequence
    MD5 Hash
    MD5 Hash
    Length
    Length
    KDBX 3.1 File
    KDBX 3.1 File
    Header Format
    Header Format
    Hashed Block
    Format
    Hashed Block<div>Format</div>
    Unencrypted
    Payload
    (Optionally Compressed)
    [Not supported by viewer]
    36
    36
    0
    0
    40
    40
    12
    12
    1
    1
    3
    3
    0
    0
    Byte
    Offset
    [Not supported by viewer]
    Byte
    Offset
    [Not supported by viewer]
    Byte
    Offset
    [Not supported by viewer]
    Hashed Blocks
    Hashed Blocks
    Magic Number
    Magic Number
    Version
    Version
    Header
    Header
    Header
    Header
    Header
    Header
    Header 0
    Header 0
    ...
    ...
    0
    0
    8
    8
    32
    32
    Type
    Type
    Length
    Length
    Value
    Value
    Unencrypted
    Unencrypted
    Length
    Length
    Header Format
    Header Format
    Hashed Block
    Format
    Hashed Block<div>Format</div>
    HMAC-SHA-256
    HMAC-SHA-256
    36
    36
    0
    0
    12
    12
    1
    1
    5
    5
    0
    0
    Byte
    Offset
    [Not supported by viewer]
    Byte
    Offset
    [Not supported by viewer]
    Byte
    Offset
    [Not supported by viewer]
    KDBX 4 File
    KDBX 4 File
    http://keepass.info/help/kb/kdbx_4.html
    http://keepass.info/help/kb/kdbx_4.html
    HMAC-SHA-256
    HMAC-SHA-256
    Encrypted
    Payload
    (Optionally Compressed)
    [Not supported by viewer]
    \ No newline at end of file +
    Magic Number
    Magic Number
    Version
    Version
    Header
    Header
    Header
    Header
    Header
    Header
    Header 0
    Header 0
    ...
    ...
    Value of Header 9
    Value of Header 9
    0
    0
    8
    8
    4
    4
    Type
    Type
    Length
    Length
    Value
    Value
    Encrypted
    Encrypted
    Unencrypted
    Unencrypted
    Sequence
    Sequence
    MD5 Hash
    MD5 Hash
    Length
    Length
    KDBX 3.1 File
    KDBX 3.1 File
    Header Format
    Header Format
    Hashed Block
    Format
    Hashed Block<div>Format</div>
    Unencrypted
    Payload
    (Optionally Compressed)
    [Not supported by viewer]
    36
    36
    0
    0
    40
    40
    12
    12
    1
    1
    3
    3
    0
    0
    Byte
    Offset
    [Not supported by viewer]
    Byte
    Offset
    [Not supported by viewer]
    Byte
    Offset
    [Not supported by viewer]
    Magic Number
    Magic Number
    Version
    Version
    Header
    Header
    Header
    Header
    Header
    Header
    Header 0
    Header 0
    ...
    ...
    0
    0
    8
    8
    32
    32
    Type
    Type
    Length
    Length
    Value
    Value
    Unencrypted
    Unencrypted
    Length
    Length
    Header Format
    Header Format
    Hashed Block
    Format
    Hashed Block<div>Format</div>
    HMAC-SHA-256
    HMAC-SHA-256
    36
    36
    0
    0
    12
    12
    1
    1
    5
    5
    0
    0
    Byte
    Offset
    [Not supported by viewer]
    Byte
    Offset
    [Not supported by viewer]
    Byte
    Offset
    [Not supported by viewer]
    KDBX 4 File
    KDBX 4 File
    http://keepass.info/help/kb/kdbx_4.html
    http://keepass.info/help/kb/kdbx_4.html
    SHA-256
    SHA-256
    Encrypted
    Payload
    (Optionally Compressed)
    [Not supported by viewer]
    HMAC-SHA-256
    HMAC-SHA-256
    Inner Header
    Inner Header
    Hashed Blocks
    Hashed Blocks
    Inner Header
    Inner Header
    ...
    ...
    Header 0
    Header 0
    XML Database
    XML Database
    Hashed Blocks
    Hashed Blocks
    XML Database
    XML Database
    \ No newline at end of file From 613b0fddc8c1ca2ce6d8602e3e2649dfaa7df463 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 10 Feb 2018 14:12:22 +0000 Subject: [PATCH 071/228] Updating documentation --- FormatDiagram.svg => KdbxDiagram.svg | 0 readme.md | 14 +++++++------- 2 files changed, 7 insertions(+), 7 deletions(-) rename FormatDiagram.svg => KdbxDiagram.svg (100%) diff --git a/FormatDiagram.svg b/KdbxDiagram.svg similarity index 100% rename from FormatDiagram.svg rename to KdbxDiagram.svg diff --git a/readme.md b/readme.md index 0070b59d..ab19c762 100644 --- a/readme.md +++ b/readme.md @@ -102,12 +102,12 @@ The Database has a root group and by following sub-groups of the root group the database.newGroup(groupToCopy); The class Javadoc on Interface classes -[Database](database/src/main/java/org/linguafranca/pwdb/Database.java), -[Group](database/src/main/java/org/linguafranca/pwdb/Group.java) and -[Entry](database/src/main/java/org/linguafranca/pwdb/Entry.java) describe +[Database](http://javadoc.io/page/org.linguafranca.pwdb/database/latest/org/linguafranca/pwdb/Database.html), +[Group](http://javadoc.io/page/org.linguafranca.pwdb/database/latest/org/linguafranca/pwdb/Group.html) and +[Entry](http://javadoc.io/page/org.linguafranca.pwdb/database/latest/org/linguafranca/pwdb/Entry.html) describe how to use the methods of those classes to create and modify entries. These classes provide the basis of all implementations of the various database formats, -initially KDB and KDBX 3.1 (KeePass 2) file formats, subsequently, potentially, others. +initially KDB, KDBX 3.1 and KDBX 4 (KeePass 2) file formats, subsequently, potentially, others. The class [QuickStart.java](example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java) provides some illustrations of operations using the Database, Group and Entry interfaces. @@ -125,10 +125,10 @@ KeePass is in effect defined by the code that Dominik writes to create and maint Hence there is no definitive specification of KeePass files other than that code. For the sake of clarification and my own satisfaction I have written about my understanding of KeePass formats in the following locations: -1. The Javadoc header to [KdbxSerializer](http://static.javadoc.io/org.linguafranca.pwdb/KeePassJava2-kdbx/2.1.4/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.html) describes version 3.1 KDBX stream formatting. -2. The XSD Schema [KDBX.3.1.xsd](KDBX.3.1.xsd) documents my understanding of the Keepass XML, and also my lack of understanding, in parts. +1. The Javadoc header to [KdbxSerializer](http://javadoc.io/page/org.linguafranca.pwdb/KeePassJava2-kdbx/latest/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.html) describes KDBX stream formatting. +2. The XSD Schema [KDBX.4.xsd](KDBX.4.xsd) documents my understanding of the Keepass XML, and also my lack of understanding, in parts. 3. The following graphic illustrates KDBX 3.1 and 4 file formats: -[![KDBX Formats](./FormatDiagram.svg "KDBX Formats")](./FormatDiagram.svg) +[![KDBX Formats](KdbxDiagram.svg "KDBX Formats")](KdbxDiagram.svg) ## Dependencies From 08ce27e41478605d553997a31e6015d72f9831e7 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sun, 11 Feb 2018 10:08:08 +0000 Subject: [PATCH 072/228] Refactoring all the encryption things and tidy of documentation --- .../org/linguafranca/pwdb/Credentials.java | 5 +- .../java/org/linguafranca/pwdb/Entry.java | 7 +- .../java/org/linguafranca/pwdb/Visitor.java | 2 +- .../org/linguafranca/pwdb/security/Aes.java | 86 ++++++++- .../org/linguafranca/pwdb/security/Argon.java | 62 ------ .../linguafranca/pwdb/security/Argon2.java | 76 ++++++++ .../linguafranca/pwdb/security/ChaCha.java | 45 ++++- .../pwdb/security/CipherAlgorithm.java | 36 ++++ .../pwdb/security/Encryption.java | 179 +++++++++++------- .../pwdb/security/KeyDerivationFunction.java | 22 +++ .../pwdb/security/VariantDictionary.java | 156 ++++++++++++--- .../pwdb/kdbx/jaxb/JaxbDatabase.java | 9 +- .../pwdb/kdbx/jaxb/JaxbEntry.java | 3 +- .../org/linguafranca/pwdb/kdb/KdbEntry.java | 3 +- .../org/linguafranca/pwdb/kdb/KdbHeader.java | 7 +- .../hashedblock/CollectingInputStream.java | 28 ++- .../hashedblock/HmacBlockInputStream.java | 111 +++++------ .../org/linguafranca/pwdb/kdbx/Helpers.java | 16 ++ .../linguafranca/pwdb/kdbx/KdbxHeader.java | 140 +++++++++----- .../pwdb/kdbx/KdbxSerializer.java | 58 +++--- .../pwdb/kdbx/KdbxStreamFormat.java | 18 +- .../pwdb/kdbx/stream/KdbxSerializerTest.java | 5 +- readme.md | 5 +- .../pwdb/kdbx/simple/SimpleEntry.java | 3 +- 24 files changed, 751 insertions(+), 331 deletions(-) delete mode 100644 database/src/main/java/org/linguafranca/pwdb/security/Argon.java create mode 100644 database/src/main/java/org/linguafranca/pwdb/security/Argon2.java create mode 100644 database/src/main/java/org/linguafranca/pwdb/security/CipherAlgorithm.java create mode 100644 database/src/main/java/org/linguafranca/pwdb/security/KeyDerivationFunction.java diff --git a/database/src/main/java/org/linguafranca/pwdb/Credentials.java b/database/src/main/java/org/linguafranca/pwdb/Credentials.java index f09f475e..7f03b64e 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Credentials.java +++ b/database/src/main/java/org/linguafranca/pwdb/Credentials.java @@ -38,5 +38,8 @@ public byte[] getKey() { } } - byte [] getKey(); + /** + * Returns a digest of the composition of credentials supplied + */ + byte[] getKey(); } diff --git a/database/src/main/java/org/linguafranca/pwdb/Entry.java b/database/src/main/java/org/linguafranca/pwdb/Entry.java index 1b5ddcc2..4780a0ff 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Entry.java +++ b/database/src/main/java/org/linguafranca/pwdb/Entry.java @@ -17,6 +17,7 @@ package org.linguafranca.pwdb; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.*; @@ -183,9 +184,9 @@ interface Matcher { /** * Get the parent of this entry - * @return a parent + * @return a parent or null if this entry currently does not have a parent */ - @NotNull G getParent(); + @Nullable G getParent(); /** * Get the UUID of this entry. Databases (like KDB) that do not natively support @@ -358,7 +359,7 @@ interface Matcher { /** * Returns the date that the entry was last modified *

    - * Implementations SHOULD set this to the creation date or earlier if the entry has never been used. + * Implementations SHOULD set this to the creation date or earlier if the entry has never been used. */ Date getLastModificationTime(); } diff --git a/database/src/main/java/org/linguafranca/pwdb/Visitor.java b/database/src/main/java/org/linguafranca/pwdb/Visitor.java index ca3090d9..ac3493c2 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Visitor.java +++ b/database/src/main/java/org/linguafranca/pwdb/Visitor.java @@ -67,7 +67,7 @@ public boolean isEntriesFirst() { } /** - * Visitor prints the Groups and Entries it visits + * Visitor prints the Groups and Entries it visits to console */ class Print extends Default { @Override diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java index 5d65c0ad..ba61191a 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java @@ -1,24 +1,38 @@ package org.linguafranca.pwdb.security; import org.spongycastle.crypto.engines.AESEngine; +import org.spongycastle.crypto.io.CipherInputStream; +import org.spongycastle.crypto.io.CipherOutputStream; import org.spongycastle.crypto.modes.CBCBlockCipher; import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; import org.spongycastle.crypto.params.KeyParameter; +import org.spongycastle.crypto.params.ParametersWithIV; +import java.io.InputStream; +import java.io.OutputStream; import java.security.MessageDigest; +import java.security.SecureRandom; import java.util.UUID; +import static org.linguafranca.pwdb.security.Aes.KdfKeys.ParamRounds; +import static org.linguafranca.pwdb.security.Aes.KdfKeys.ParamSeed; import static org.linguafranca.pwdb.security.Encryption.getSha256MessageDigestInstance; /** * AES may be used for Key Derivation and also as the underlying stream cipher - * - * @author jo + *

    + * The class is a singleton */ -public class Aes { +public class Aes implements CipherAlgorithm, KeyDerivationFunction { - /** UUID specifying that AES is to be used as the Key Derivation Function */ - public static final UUID KDF = UUID.fromString("C9D9F39A-628A-4460-BF74-0D08C18A4FEA"); + /** UUID specifying that AES is to be used as the Key Derivation Function in KDBX */ + private static final UUID KDF = UUID.fromString("C9D9F39A-628A-4460-BF74-0D08C18A4FEA"); + private static VariantDictionary kdfParameters = new VariantDictionary((short) 1); + static { + kdfParameters.putUuid("$UUID", KDF); + kdfParameters.putLong(ParamRounds, 6000L); + kdfParameters.putByteArray(ParamSeed, SecureRandom.getSeed(32)); + } /** v4 variant dictionary keys for use of AES as the KDF */ public static class KdfKeys { @@ -26,11 +40,67 @@ public static class KdfKeys { public static final String ParamSeed = "S"; // Byte[32] } - public static PaddedBufferedBlockCipher getCipher() { - return new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine())); + /** UUID specifying that AES is to be used as the Cipher in KDBX */ + private static final UUID CIPHER = UUID.fromString("31C1F2E6-BF71-4350-BE58-05216AFC5AFF"); + + /** hide constructor */ + private Aes () { } + private static final Aes instance = new Aes(); + + public static Aes getInstance () { + return instance; + } + + /** + * get a copy of the Aes Variant dictionary + * @return a copy + */ + public static VariantDictionary createKdfParameters() { + return kdfParameters.copy(); + } + + + @Override + public UUID getCipherUuid() { + return CIPHER; + } + + @Override + public InputStream getDecryptedInputStream(InputStream encryptedInputStream, byte[] key, byte[] iv) { + final ParametersWithIV keyAndIV = new ParametersWithIV(new KeyParameter(key), iv); + PaddedBufferedBlockCipher pbbc = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine())); + pbbc.init(false, keyAndIV); + return new CipherInputStream(encryptedInputStream, pbbc); + } + + @Override + public OutputStream getEncryptedOutputStream(OutputStream decryptedOutputStream, byte[] key, byte[] iv) { + final ParametersWithIV keyAndIV = new ParametersWithIV(new KeyParameter(key), iv); + PaddedBufferedBlockCipher pbbc = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine())); + pbbc.init(true, keyAndIV); + return new CipherOutputStream(decryptedOutputStream, pbbc); + } + + @Override + public UUID getKdfUuid() { + return KDF; + } + + @Override + public byte[] getTransformedKey(byte[] key, VariantDictionary transformParams) { + return getTransformedKey(key, + transformParams.mustGet(ParamSeed).asByteArray(), + transformParams.mustGet(ParamRounds).asLong()); } - public static byte [] getTransformedKey (byte [] key, byte [] transformSeed, long transformRounds) { + /** + * Simplified version for KDBX V3 + * @param key they composite key + * @param transformSeed the seed + * @param transformRounds number of rounds + * @return a transformed key + */ + public static byte[] getTransformedKey(byte[] key, byte [] transformSeed, long transformRounds) { AESEngine engine = new AESEngine(); engine.init(true, new KeyParameter(transformSeed)); diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Argon.java b/database/src/main/java/org/linguafranca/pwdb/security/Argon.java deleted file mode 100644 index 847ceddd..00000000 --- a/database/src/main/java/org/linguafranca/pwdb/security/Argon.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.linguafranca.pwdb.security; - -import com.kosprov.jargon2.api.Jargon2; - -import java.security.MessageDigest; -import java.util.UUID; - -import static com.kosprov.jargon2.api.Jargon2.*; -import static org.linguafranca.pwdb.security.Argon.ArgonParameterKeys.ArgonKeys.*; -import static org.linguafranca.pwdb.security.Encryption.getSha256MessageDigestInstance; - - -/** - * KDBX V4 files may use Argon2 for key derivation. - * - * @author jo - */ -public class Argon { - - /** UUID indicating that Argon is being used as the KDF */ - public static final UUID argon2_kdf = UUID.fromString("EF636DDF-8C29-444B-91F7-A9A403E30A0C"); - - /** keys into the variant dictionary supplied as a KDBX header */ - public static class ArgonParameterKeys { - @SuppressWarnings("unused") - public static class ArgonKeys { - static final String paramSalt = "S"; // Byte[] - static final String paramParallelism = "P"; // UInt32 - static final String paramMemory = "M"; // UInt64 - static final String paramIterations = "I"; // UInt64 - static final String paramVersion = "V"; // UInt32 - static final String paramSecretKey = "K"; // Byte[] - static final String paramAssocData = "A"; // Byte[] - - static final int minVersion = 0x10; - static final int maxVersion = 0x13; - } - } - - - public static byte[] getTransformedKey(byte[] digest, VariantDictionary argonParameterKeys) { - byte bVersion = argonParameterKeys.get(paramVersion).asByteArray()[0]; - Version version = bVersion == 0x13 ? Version.V13 : Version.V10; - byte [] salt = argonParameterKeys.get(paramSalt).asByteArray(); - int parallelism = argonParameterKeys.get(paramParallelism).asInteger(); - int memoryCost = (int) argonParameterKeys.get(paramMemory).asLong(); - int timeCost = (int) argonParameterKeys.get(paramIterations).asLong(); - - // Configure the hasher - Hasher hasher = jargon2Hasher() - .type(Type.ARGON2d) - .version(version) - .salt(salt) - .parallelism(parallelism) - .memoryCost(memoryCost/1024) // block size 1024 - .timeCost(timeCost) - .hashLength(32); - - // do the hash - return hasher.password(digest).rawHash(); - } -} diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java b/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java new file mode 100644 index 00000000..07166d61 --- /dev/null +++ b/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java @@ -0,0 +1,76 @@ +package org.linguafranca.pwdb.security; + +import java.util.UUID; + +import static com.kosprov.jargon2.api.Jargon2.*; +import static org.linguafranca.pwdb.security.Argon2.VariantDictionaryKeys.*; + + +/** + * KDBX V4 files may use Argon2 for key derivation. + *

    + * A singleton + */ +public class Argon2 implements KeyDerivationFunction { + + /** + * UUID indicating that Argon is being used as the KDF + */ + private static final UUID argon2_kdf = UUID.fromString("EF636DDF-8C29-444B-91F7-A9A403E30A0C"); + + /** + * hide constructor + */ + private Argon2() { + } + + private static final Argon2 instance = new Argon2(); + + public static Argon2 getInstance() { + return instance; + } + + + /** + * keys into the variant dictionary supplied as a KDBX header + */ + @SuppressWarnings("WeakerAccess") + public static class VariantDictionaryKeys { + static final String paramSalt = "S"; // Byte[] + static final String paramParallelism = "P"; // UInt32 + static final String paramMemory = "M"; // UInt64 + static final String paramIterations = "I"; // UInt64 + static final String paramVersion = "V"; // UInt32 + static final String paramSecretKey = "K"; // Byte[] + static final String paramAssocData = "A"; // Byte[] + } + + + @Override + public UUID getKdfUuid() { + return argon2_kdf; + } + + @Override + public byte[] getTransformedKey(byte[] digest, VariantDictionary argonParameterKeys) { + byte bVersion = argonParameterKeys.mustGet(paramVersion).asByteArray()[0]; + Version version = bVersion == 0x13 ? Version.V13 : Version.V10; + byte[] salt = argonParameterKeys.mustGet(paramSalt).asByteArray(); + int parallelism = argonParameterKeys.mustGet(paramParallelism).asInteger(); + int memoryCost = (int) argonParameterKeys.mustGet(paramMemory).asLong(); + int timeCost = (int) argonParameterKeys.mustGet(paramIterations).asLong(); + + // Configure the hasher + Hasher hasher = jargon2Hasher() + .type(Type.ARGON2d) + .version(version) + .salt(salt) + .parallelism(parallelism) + .memoryCost(memoryCost / 1024) // block size 1024 + .timeCost(timeCost) + .hashLength(32); + + // do the hash + return hasher.password(digest).rawHash(); + } +} diff --git a/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java b/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java index a929397b..558e1900 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java @@ -2,15 +2,50 @@ import org.spongycastle.crypto.StreamCipher; import org.spongycastle.crypto.engines.ChaCha7539Engine; +import org.spongycastle.crypto.io.CipherInputStream; +import org.spongycastle.crypto.io.CipherOutputStream; +import org.spongycastle.crypto.params.KeyParameter; +import org.spongycastle.crypto.params.ParametersWithIV; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.UUID; /** * ChaCha20 may be used for the inner stream as well as for the underlying database encryption scheme - * - * @author jo + *

    + * A singleton */ -public class ChaCha { +public class ChaCha implements CipherAlgorithm { + + private static final UUID CHACHA_CIPHER = UUID.fromString("d6038a2b-8b6f-4cb5-a524-339a31dbb59a"); + + // hide constructor to enforce singleton + private ChaCha(){} + private static ChaCha instance = new ChaCha(); + + public static ChaCha getInstance() { + return instance; + } + + @Override + public UUID getCipherUuid() { + return CHACHA_CIPHER; + } + + @Override + public InputStream getDecryptedInputStream(InputStream encryptedInputStream, byte[] key, byte[] iv) { + final ParametersWithIV keyAndIV = new ParametersWithIV(new KeyParameter(key), iv); + StreamCipher cipher = new ChaCha7539Engine(); + cipher.init(false, keyAndIV); + return new CipherInputStream(encryptedInputStream, cipher); + } - public static StreamCipher getCipher () { - return new ChaCha7539Engine(); + @Override + public OutputStream getEncryptedOutputStream(OutputStream decryptedOutputStream, byte[] key, byte[] iv) { + final ParametersWithIV keyAndIV = new ParametersWithIV(new KeyParameter(key), iv); + StreamCipher cipher = new ChaCha7539Engine(); + cipher.init(true, keyAndIV); + return new CipherOutputStream(decryptedOutputStream, cipher); } } diff --git a/database/src/main/java/org/linguafranca/pwdb/security/CipherAlgorithm.java b/database/src/main/java/org/linguafranca/pwdb/security/CipherAlgorithm.java new file mode 100644 index 00000000..970b78ef --- /dev/null +++ b/database/src/main/java/org/linguafranca/pwdb/security/CipherAlgorithm.java @@ -0,0 +1,36 @@ +package org.linguafranca.pwdb.security; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.UUID; + +/** + * Interface defining an algorithm for encrypting and decrypting database contents + */ +public interface CipherAlgorithm { + /** + * Returns the UUID of this algorithm + */ + UUID getCipherUuid(); + + /** + * Create a decrypted stream from the supplied encruypted one + * + * @param encryptedInputStream an encryted stream + * @param key the decryption key + * @param iv the iv + * @return an unencrypted stream + */ + InputStream getDecryptedInputStream(InputStream encryptedInputStream, byte[] key, byte[] iv); + + /** + * Create an encrypted stream from the supplied unencrypted one + * + * @param decryptedOutputStream an unencrypted stream + * @param key a key + * @param iv an iv + * @return an encrypted stream + */ + OutputStream getEncryptedOutputStream(OutputStream decryptedOutputStream, byte[] key, byte[] iv); + +} diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java b/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java index ed957d2c..60c94e99 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java @@ -16,66 +16,23 @@ package org.linguafranca.pwdb.security; -// use spongycastle repackaging of bouncycastle in deference to Android needs -import org.spongycastle.crypto.StreamCipher; -import org.spongycastle.crypto.engines.AESFastEngine; -import org.spongycastle.crypto.io.CipherInputStream; -import org.spongycastle.crypto.io.CipherOutputStream; -import org.spongycastle.crypto.modes.CBCBlockCipher; -import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; -import org.spongycastle.crypto.params.KeyParameter; -import org.spongycastle.crypto.params.ParametersWithIV; - -import java.io.IOException; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; import java.io.InputStream; import java.io.OutputStream; -import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.UUID; /** - * Encryption and decryption utilities.. - * - * @author jo + * Contains the key transform functions and cipher algorithms used in other modules. + *

    + * Also some convenience utilities that hide the checked exceptions that would otherwise need to be checked for + * when using digests. */ public class Encryption { - /** - * Gets a digest for a UTF-8 encoded string - * - * @param string the string - * @return a digest as a byte array - */ - @SuppressWarnings("unused") - public static byte[] getDigest(String string) { - return getDigest(string, "UTF-8"); - } - - /** - * Gets a digest for a string - * - * @param string the string - * @param encoding the encoding of the String - * @return a digest as a byte array - */ - public static byte[] getDigest(String string, String encoding) { - if (string == null || string.length() == 0) - throw new IllegalArgumentException("String cannot be null or empty"); - - if (encoding == null || encoding.length() == 0) - throw new IllegalArgumentException("Encoding cannot be null or empty"); - - MessageDigest md = getSha256MessageDigestInstance(); - - try { - byte[] bytes = string.getBytes(encoding); - md.update(bytes, 0, bytes.length); - return md.digest(); - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException(encoding + " is not supported"); - } - } - /** * Gets a SHA-256 message digest instance * @@ -103,30 +60,118 @@ public static MessageDigest getSha512MessageDigestInstance() { } /** - * Create a decrypted input stream from an encrypted one + * Gets an HMacSha256 Mac + * + * @param key the key + * @return the Mac initialised with the key + */ + public static Mac getHMacSha256Instance(byte[] key) { + try { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(key, "HmacSHA256")); + return mac; + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new IllegalStateException("HmacSHA256 is not supported", e); + } + } + + /** + * From HmacBlockStream.cs GetHmacKey64 + * Calculates the block key for the block number ... + * + * @param digest the HMAC key digest + * @param transform the transform seed (the block number, 8 bytes) + * @return a key */ - public static InputStream getDecryptedInputStream(InputStream encryptedInputStream, PaddedBufferedBlockCipher pbbc, byte[] keyData, byte[] ivData) { - final ParametersWithIV keyAndIV = new ParametersWithIV(new KeyParameter(keyData), ivData); - pbbc.init(false, keyAndIV); - return new CipherInputStream(encryptedInputStream, pbbc); + public static byte[] transformHmacKey(byte[] digest, byte[] transform) { + MessageDigest md = Encryption.getSha512MessageDigestInstance(); + md.update(transform); + return md.digest(digest); } /** - * Create a decrypted input stream from an encrypted one + * A list of functions that we can use to transform keys + * Enum constants forward to underlying implementation. */ - public static InputStream getDecryptedInputStream(InputStream encryptedInputStream, StreamCipher cipher, byte[] keyData, byte[] ivData) throws IOException { - final ParametersWithIV keyAndIV = new ParametersWithIV(new KeyParameter(keyData), ivData); - cipher.init(false, keyAndIV); - return new CipherInputStream(encryptedInputStream, cipher); + public enum Kdf implements KeyDerivationFunction { + AES(Aes.getInstance()), + ARGON2(Argon2.getInstance()); + + private final KeyDerivationFunction kdf; + + Kdf(KeyDerivationFunction kdf) { + this.kdf = kdf; + } + + /** + * Find a KDF that matches this Uuid + * + * @param kdfUuid the Uuid to match + * @throws IllegalArgumentException if the Uuid is not known + */ + public static KeyDerivationFunction getKdf(UUID kdfUuid) { + for (KeyDerivationFunction kdf : values()) { + if (kdf.getKdfUuid().equals(kdfUuid)) { + return kdf; + } + } + throw new IllegalArgumentException("Unknown Cipher UUID"); + } + + @Override + public UUID getKdfUuid() { + return kdf.getKdfUuid(); + } + + @Override + public byte[] getTransformedKey(byte[] key, VariantDictionary transformParams) { + return kdf.getTransformedKey(key, transformParams); + } } /** - * Create an encrypted output stream from an unencrypted output stream + * A list of ciphers that we may apply to the database contents. + * Enum constants forward to underlying implementation. */ - public static OutputStream getEncryptedOutputStream (OutputStream decryptedOutputStream, byte[] keyData, byte[] ivData) { - final ParametersWithIV keyAndIV = new ParametersWithIV(new KeyParameter(keyData), ivData); - PaddedBufferedBlockCipher pbbc = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); - pbbc.init(true, keyAndIV); - return new CipherOutputStream(decryptedOutputStream, pbbc); + public enum Cipher implements CipherAlgorithm { + CHACHA(ChaCha.getInstance()), + AES(Aes.getInstance()); + + private final CipherAlgorithm ef; + + /** + * Find a cipher that matches this Uuid + * + * @param cipherUuid the Uuid + * @return a cipher + * @throws IllegalArgumentException if the Uuid is not known + */ + public static CipherAlgorithm getCipherAlgorithm(UUID cipherUuid) { + for (CipherAlgorithm ca : values()) { + if (ca.getCipherUuid().equals(cipherUuid)) { + return ca; + } + } + throw new IllegalArgumentException("Unknown Cipher UUID"); + } + + Cipher(CipherAlgorithm ef) { + this.ef = ef; + } + + @Override + public UUID getCipherUuid() { + return ef.getCipherUuid(); + } + + @Override + public InputStream getDecryptedInputStream(InputStream encryptedInputStream, byte[] key, byte[] iv) { + return ef.getDecryptedInputStream(encryptedInputStream, key, iv); + } + + @Override + public OutputStream getEncryptedOutputStream(OutputStream decryptedOutputStream, byte[] key, byte[] iv) { + return ef.getEncryptedOutputStream(decryptedOutputStream, key, iv); + } } } diff --git a/database/src/main/java/org/linguafranca/pwdb/security/KeyDerivationFunction.java b/database/src/main/java/org/linguafranca/pwdb/security/KeyDerivationFunction.java new file mode 100644 index 00000000..408ead7d --- /dev/null +++ b/database/src/main/java/org/linguafranca/pwdb/security/KeyDerivationFunction.java @@ -0,0 +1,22 @@ +package org.linguafranca.pwdb.security; + +import java.util.UUID; + +/** + * Interface defining a Key Derivation Function + */ +public interface KeyDerivationFunction { + /** + * The UUID of this key derivation function + */ + UUID getKdfUuid(); + + /** + * Transform a key using this key derivation faunction + * + * @param key the key to transform + * @param transformParams the parameters defining the way the tranformation is to be carried out + * @return a transformed key + */ + byte[] getTransformedKey(byte[] key, VariantDictionary transformParams); +} diff --git a/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java b/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java index 3b329342..318e95b8 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java @@ -1,17 +1,23 @@ package org.linguafranca.pwdb.security; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import javax.annotation.concurrent.Immutable; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.UUID; +import static org.linguafranca.pwdb.security.VariantDictionary.EntryType.ARRRAY; +import static org.linguafranca.pwdb.security.VariantDictionary.EntryType.UINT64; + /** * Implementation of a storage for V4 KDBX Header field parameters - * - * @author jo + *

    + * Though specific to KDBX V4 it is kept here for convenience of parameter passing to crypto functions */ @SuppressWarnings("WeakerAccess") @@ -20,25 +26,51 @@ public class VariantDictionary { private short version; private Map entries = new HashMap<>(); + /** + * The list of permissible entry types + */ @SuppressWarnings("unused") - public static class Types { - public static final byte UINT32 = 0x4; - public static final byte UINT64 = 0x5; - public static final byte BOOL = 0x8; - public static final byte INT32 = 0xC; - public static final byte INT64 = 0xD; - public static final byte STRING = 0x18; // UTF-8, without BOM, without null terminator - public static final byte ARRRAY = 0x42; + public enum EntryType { + UINT32(0x4), + UINT64(0x5), + BOOL(0x8), + INT32(0xC), + INT64(0xD), + STRING(0x18), // UTF-8, without BOM, without null terminator + ARRRAY(0x42); + + private final byte value; + + EntryType(int b) { + this.value = (byte) b; + } + + public static EntryType get(byte type) { + for (EntryType et : values()) { + if (et.value == type) { + return et; + } + } + throw new IllegalArgumentException("Unknown Variant Dictionary Type " + String.format("%x", type)); + } } + @SuppressWarnings("WeakerAccess") + @Immutable public static class Entry { - private byte type; - private byte [] value; + private final byte type; + private final @NotNull byte[] value; + private final ByteOrder byteOrder; - public Entry(byte type, byte [] value) { - this.type = type; - this.value = value; + public Entry(EntryType entryType, @NotNull byte[] value) { + this(entryType, value, ByteOrder.LITTLE_ENDIAN); + } + + public Entry(EntryType entryType, @NotNull byte[] value, ByteOrder byteOrder) { + this.type = entryType.value; + this.value = Objects.requireNonNull(value); + this.byteOrder = byteOrder; } public byte getType() { @@ -54,36 +86,114 @@ public UUID asUuid() { } public long asLong() { - if (value.length != 8){ + if (value.length != 8) { throw new IllegalStateException("Cannot convert value to long"); } - return ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).getLong(); + return ByteBuffer.wrap(value).order(byteOrder).getLong(); } public int asInteger() { - if (value.length != 4){ + if (value.length != 4) { throw new IllegalStateException("Cannot convert value to int"); } - return ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).getInt(); + return ByteBuffer.wrap(value).order(byteOrder).getInt(); } - public byte [] asByteArray() { + public @NotNull byte[] asByteArray() { return value; } } + /** + * Make a new Variant Dictionary whose version must be 1 + */ public VariantDictionary(short version) { + if (version != 1) { + throw new IllegalArgumentException("Variant Dictionary version must be 1"); + } this.version = version; } + + /** + * Make a copy of this structure - Entries are immutable so are copied as is + */ + public VariantDictionary copy() { + VariantDictionary vd = new VariantDictionary(this.version); + for (Map.Entry e : this.entries.entrySet()) { + vd.entries.put(e.getKey(), e.getValue()); + } + return null; + } + + /** + * Get the version number of this structure + * + * @return 1 + */ public short getVersion() { return version; } - public @Nullable Entry get(String key) { + /** + * Return an entry for the key supplied + * + * @param key the key + * @return an entry, or null if no such entry exists + */ + public @Nullable Entry get(@NotNull String key) { return entries.get(key); } - public void put(String key, byte type, byte [] value) { - entries.put(key, new Entry(type, value)); + /** + * ensure that the entry sought is not null, by throwing an illegal argument exception if it is not present + * + * @param key the key to get + * @return the entry corresponding to the key + */ + public @NotNull Entry mustGet(@NotNull String key) { + Entry entry = entries.get(key); + if (entry == null) { + throw new IllegalArgumentException("There is no entry with key " + key); + } + return entry; + } + + /** + * Add an entry of the type defined + * + * @param key the entry key + * @param type the data type of the entry + * @param value a buffer containing an appropriate entry + */ + public void put(@NotNull String key, EntryType type, byte[] value) { + entries.put(Objects.requireNonNull(key), new Entry(type, value)); + } + + /** + * Put a UUID under the key defined + */ + public void putUuid(@NotNull String key, UUID uuid) { + byte[] buf = new byte[16]; + ByteBuffer bb = ByteBuffer.wrap(buf); + bb.putLong(0, uuid.getMostSignificantBits()); + bb.putLong(8, uuid.getLeastSignificantBits()); + entries.put(Objects.requireNonNull(key), new Entry(ARRRAY, buf)); + } + + /** + * Put a byte array under the key defined + */ + public void putByteArray(@NotNull String key, @NotNull byte[] value) { + entries.put(Objects.requireNonNull(key), new Entry(ARRRAY, value)); + } + + /** + * Put a long as an unsigned64 undewr the key defined + */ + public void putLong(@NotNull String key, long value) { + byte[] buf = new byte[8]; + ByteBuffer bb = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN); + bb.putLong(value); + entries.put(Objects.requireNonNull(key), new Entry(UINT64, buf)); } } diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java index 85c785ab..3b34f151 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java @@ -17,12 +17,12 @@ package org.linguafranca.pwdb.kdbx.jaxb; import org.jetbrains.annotations.NotNull; +import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.base.AbstractDatabase; import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import org.linguafranca.pwdb.kdbx.StreamFormat; import org.linguafranca.pwdb.kdbx.jaxb.binding.KeePassFile; import org.linguafranca.pwdb.kdbx.jaxb.binding.ObjectFactory; -import org.linguafranca.pwdb.Credentials; import java.io.IOException; import java.io.InputStream; @@ -35,7 +35,7 @@ * * @author jo */ -@SuppressWarnings("WeakerAccess") + public class JaxbDatabase extends AbstractDatabase { private KeePassFile keePassFile; @@ -46,13 +46,12 @@ public JaxbDatabase() { this(createEmptyDatabase().getKeePassFile()); } - public JaxbDatabase(KeePassFile keePassFile) { + private JaxbDatabase(KeePassFile keePassFile) { this.keePassFile = keePassFile; this.root = new JaxbGroup(this, keePassFile.getRoot().getGroup()); } public static JaxbDatabase createEmptyDatabase() { - InputStream inputStream = JaxbDatabase.class.getClassLoader().getResourceAsStream("base.kdbx.xml"); KeePassFile keePassFile = new JaxbSerializableDatabase().load(inputStream).keePassFile; keePassFile.getRoot().getGroup().setUUID(UUID.randomUUID()); @@ -99,7 +98,7 @@ public JaxbGroup getRecycleBin() { if (g == null && !isRecycleBinEnabled()) { return null; } - if (g== null) { + if (g == null) { g = newGroup("Recycle Bin"); getRootGroup().addGroup(g); this.keePassFile.getMeta().setRecycleBinUUID(g.getUuid()); diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java index eb8391d0..533193ab 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java @@ -16,6 +16,7 @@ package org.linguafranca.pwdb.kdbx.jaxb; +import org.jetbrains.annotations.NotNull; import org.linguafranca.pwdb.base.AbstractEntry; import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.jaxb.binding.*; @@ -228,7 +229,7 @@ public JaxbGroup getParent() { } @Override - public UUID getUuid() { + public @NotNull UUID getUuid() { return delegate.getUUID(); } diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java index d7440b56..4cd403ad 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java @@ -16,6 +16,7 @@ package org.linguafranca.pwdb.kdb; +import org.jetbrains.annotations.NotNull; import org.linguafranca.pwdb.Entry; import org.linguafranca.pwdb.Icon; @@ -93,7 +94,7 @@ public KdbGroup getParent() { } @Override - public UUID getUuid() { + public @NotNull UUID getUuid() { return uuid; } diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbHeader.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbHeader.java index 0a67633a..976bf318 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbHeader.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbHeader.java @@ -17,10 +17,8 @@ package org.linguafranca.pwdb.kdb; import org.linguafranca.pwdb.security.Aes; -import org.linguafranca.pwdb.security.Encryption; import javax.crypto.Cipher; -import java.io.IOException; import java.io.InputStream; import java.security.MessageDigest; @@ -57,9 +55,8 @@ public class KdbHeader { * @param key key * @param inputStream an encrypted stream * @return a decrypted stream - * @throws IOException */ - public InputStream createDecryptedInputStream(byte[] key, InputStream inputStream) throws IOException { + public InputStream createDecryptedInputStream(byte[] key, InputStream inputStream) { Cipher cipher; if ((flags & FLAG_RIJNDAEL) == 0) { throw new IllegalStateException("StreamEncryptor algorithm is not supported"); @@ -69,7 +66,7 @@ public InputStream createDecryptedInputStream(byte[] key, InputStream inputStrea MessageDigest md = getSha256MessageDigestInstance(); md.update(masterSeed); byte[] finalKeyDigest = md.digest(transformedKeyDigest); - return Encryption.getDecryptedInputStream(inputStream, Aes.getCipher(), finalKeyDigest, encryptionIv); + return Aes.getInstance().getDecryptedInputStream(inputStream, finalKeyDigest, encryptionIv); } public int getFlags() { diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingInputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingInputStream.java index 9dd32ee7..e932f62c 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingInputStream.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingInputStream.java @@ -8,7 +8,8 @@ import java.io.InputStream; /** - * Class allows the forwarding and collection of read bytes as a buffer - e.g. to provide for HMAC operations + * Class allows the forwarding (as a filter) and collection of read bytes as a buffer + * - e.g. to provide for HMAC operations * * @author jo */ @@ -17,10 +18,21 @@ public class CollectingInputStream extends FilterInputStream { private ByteArrayOutputStream collectedBytes = new ByteArrayOutputStream(); private boolean collecting = true; + /** + * Create a collecting stream which is set to collect from the get go + * + * @param in the input stream to forward/collect + */ protected CollectingInputStream(InputStream in) { this(in, true); } + /** + * Create a collecting stream + * + * @param in the input stream to forward/collect + * @param collecting whether the initial state is collecting or not + */ public CollectingInputStream(InputStream in, boolean collecting) { super(in); this.collecting = collecting; @@ -28,7 +40,7 @@ public CollectingInputStream(InputStream in, boolean collecting) { @Override public int read() throws IOException { - int result = super.read(); + int result = super.read(); if (collecting && result != -1) { collectedBytes.write(result); } @@ -54,14 +66,24 @@ public long skip(long n) throws IOException { return super.skip(n); } - public byte [] getCollectedBytes() { + /** + * Get the collected bytes as a byte array + */ + public byte[] getCollectedBytes() { return collectedBytes.toByteArray(); } + /** + * True if we are currently collecting bytes + */ public boolean isCollecting() { return collecting; } + /** + * Cganeg the state of collecting bytes + * @param collecting true to collect + */ public void setCollecting(boolean collecting) { this.collecting = collecting; } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java index 1dba6fd4..07bbd1c2 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java @@ -2,22 +2,32 @@ import com.google.common.io.LittleEndianDataInputStream; import org.jetbrains.annotations.NotNull; -import org.linguafranca.pwdb.security.Encryption; -import org.spongycastle.crypto.digests.SHA256Digest; -import org.spongycastle.crypto.macs.HMac; -import org.spongycastle.crypto.params.KeyParameter; +import javax.crypto.Mac; import java.io.*; -import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.security.MessageDigest; import java.util.Arrays; +import static org.linguafranca.pwdb.kdbx.Helpers.toBytes; +import static org.linguafranca.pwdb.security.Encryption.getHMacSha256Instance; +import static org.linguafranca.pwdb.security.Encryption.transformHmacKey; + /** * Takes an underlying stream formatted as HMAC Hashed Blocks and provides * the content of the blocks as a stream. - * - * @author jo + *

    + * An HMac block consists of + *

      + *
    1. a 32 byte HMac checksum
    2. + *
    3. a 4 byte block size
    4. + *
    5. {blockSize} bytes of data
    6. + *
    + *

    + * The Class is initialised with an initial key digest. For each block this key is transformed + * using the (implied, starting from 0) block number and used as key for the block verification process. + * That process consists of digesting the block number, its length and its content. + *

    + * KeePass streams are Little Endian. */ public class HmacBlockInputStream extends FilterInputStream { @@ -28,11 +38,26 @@ public class HmacBlockInputStream extends FilterInputStream { private boolean finished; private int blockCount = 0; - public HmacBlockInputStream(byte [] key, InputStream inputStream) throws IOException { + /** + * Create a (big endian) HMac Block input stream + * + * @param key the key digest + * @param inputStream the stream to process + * @throws IOException when something horrid happens + */ + public HmacBlockInputStream(byte[] key, InputStream inputStream) throws IOException { this(key, inputStream, false); } - public HmacBlockInputStream(byte [] key, InputStream inputStream, boolean littleEndian) throws IOException { + /** + * Create an HMac Block input stream + * + * @param key the key digest + * @param inputStream the stream to process + * @param littleEndian true if the stream is little endian + * @throws IOException when something horrid happens + */ + public HmacBlockInputStream(byte[] key, InputStream inputStream, boolean littleEndian) throws IOException { super(inputStream); this.byteOrder = littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN; this.key = key; @@ -46,7 +71,7 @@ public HmacBlockInputStream(byte [] key, InputStream inputStream, boolean little private void getBlock() throws IOException { // get the HMAC - byte [] hmacSha256 = new byte [32]; + byte[] hmacSha256 = new byte[32]; input.readFully(hmacSha256); // get the block size @@ -56,69 +81,49 @@ private void getBlock() throws IOException { } // read the new block - byte [] buffer = new byte [blockSize]; + byte[] buffer = new byte[blockSize]; input.readFully(buffer); - verifyHmac(buffer, hmacSha256, blockCount); + verifyHmac(buffer, blockCount, hmacSha256); // create a new internal stream for the block bufferStream = new ByteArrayInputStream(buffer); - blockCount ++; + blockCount++; } /** * HmacBlockStream.cs ReadSafeBlock - * @param buffer - * @param hmacSha256 - * @param blockNumber + * + * @param buffer the buffer to check + * @param blockNumber the block number of this buffer + * @param hmacSha256 the hmac to verify */ - private void verifyHmac(byte[] buffer, byte[] hmacSha256, long blockNumber) { + private void verifyHmac(byte[] buffer, long blockNumber, byte[] hmacSha256) { + final byte[] transformedKey = transformHmacKey(this.key, toBytes(blockNumber, ByteOrder.LITTLE_ENDIAN)); + final Mac mac = getHMacSha256Instance(transformedKey); + mac.update(toBytes(blockNumber, byteOrder)); + mac.update(toBytes(buffer.length, byteOrder)); + if (!Arrays.equals(mac.doFinal(buffer), hmacSha256)) { + throw new IllegalStateException("Block HMAC does not match"); + } + +/* // using bouncy castle HMac hmac = new HMac(new SHA256Digest()); - hmac.init(new KeyParameter(getHmacBlockKey(key, blockNumber, byteOrder))); - hmac.update(toBytes(blockNumber, byteOrder), 0, 8); - hmac.update(toBytes(buffer.length, byteOrder), 0, 4); + hmac.init(new KeyParameter(getHmacBlockKey(this.key, blockNumber, byteOrder))); + hmac.update(Helpers.toBytes(blockNumber, byteOrder), 0, 8); + hmac.update(Helpers.toBytes(buffer.length, byteOrder), 0, 4); hmac.update(buffer, 0, buffer.length); - hmac.update(new byte[0], 0, 0); byte[] computedHmacSha256 = new byte[32]; hmac.doFinal(computedHmacSha256, 0); if (!Arrays.equals(computedHmacSha256, hmacSha256)) { throw new IllegalStateException("Block HMAC does not match"); } - } - - /** - * From HmacBlockStream.cs GetHmacKey64 - * Calculates the block key for the block number ... - * @param key the HMAC key - * @param blockIndex the block number - * @param order Byte order to use - * @return a key - */ - public static byte [] getHmacBlockKey(byte [] key, long blockIndex, ByteOrder order) { - MessageDigest md = Encryption.getSha512MessageDigestInstance(); - md.update(toBytes(blockIndex, order)); - return md.digest(key); - } - - private static byte[] toBytes(long value, ByteOrder byteOrder) { - byte[] longBuffer = new byte [8]; - ByteBuffer.wrap(longBuffer) - .order(byteOrder) - .putLong(value); - return longBuffer; - } - - private static byte[] toBytes(int value, ByteOrder byteOrder) { - byte[] longBuffer = new byte [4]; - ByteBuffer.wrap(longBuffer) - .order(ByteOrder.LITTLE_ENDIAN) - .putInt(value); - return longBuffer; +*/ } @Override public int read(@NotNull byte[] b) throws IOException { - return read(b , 0 , b.length); + return read(b, 0, b.length); } @Override diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java index 4597a534..73e5bb0a 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java @@ -148,4 +148,20 @@ public static byte[] zipBinaryContent(byte[] value) { } return baos.toByteArray(); } + + public static byte[] toBytes(long value, ByteOrder byteOrder) { + byte[] longBuffer = new byte [8]; + ByteBuffer.wrap(longBuffer) + .order(byteOrder) + .putLong(value); + return longBuffer; + } + + public static byte[] toBytes(int value, ByteOrder byteOrder) { + byte[] longBuffer = new byte [4]; + ByteBuffer.wrap(longBuffer) + .order(byteOrder) + .putInt(value); + return longBuffer; + } } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java index 044c7c18..86693b4e 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java @@ -16,9 +16,10 @@ package org.linguafranca.pwdb.kdbx; +import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.security.*; -import java.io.IOException; +import javax.crypto.Mac; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; @@ -34,12 +35,10 @@ /** * This class represents the header portion of a KeePass KDBX file or stream. The header is received in * plain text and describes the encryption and compression of the remainder of the file. - * - *

    It is a factory for encryption and decryption streams and contains a hash of its own serialization. - * + *

    + *

    It is a factory for encryption and decryption streams. It provides for verification of its own serialization. + *

    *

    While KDBX streams are Little-Endian, data is passed to and from this class in standard Java byte order. - * - * @author jo */ @SuppressWarnings("WeakerAccess") public class KdbxHeader { @@ -64,20 +63,14 @@ public enum ProtectedStreamAlgorithm { NONE, ARC_FOUR, SALSA_20, CHA_CHA_20 } - /** - * UUIDs of Ciphers for encryption - */ - public static final UUID AES_CIPHER = UUID.fromString("31C1F2E6-BF71-4350-BE58-05216AFC5AFF"); - public static final UUID CHACHA_CIPHER = UUID.fromString("d6038a2b-8b6f-4cb5-a524-339a31dbb59a"); - - private List allowableVersions = new ArrayList<>(Arrays.asList(3,4)); + private List allowableVersions = new ArrayList<>(Arrays.asList(3, 4)); /* version of the file */ private int version; protected UUID cipherUuid; - private byte [] masterSeed; + private byte[] masterSeed; private byte[] encryptionIv; /* whether the data is compressed */ @@ -96,6 +89,7 @@ public enum ProtectedStreamAlgorithm { /* dictionaries in V4 */ private VariantDictionary kdfparameters; + // TODO implement V4 custom data private VariantDictionary customData; /* not transmitted as part of the header, used in the XML payload, so calculated @@ -113,7 +107,9 @@ public KdbxHeader() { public KdbxHeader(int version) { SecureRandom random = new SecureRandom(); - cipherUuid = AES_CIPHER; + + this.version = version; + cipherUuid = Aes.getInstance().getCipherUuid(); compressionFlags = CompressionFlags.GZIP; masterSeed = random.generateSeed(32); transformSeed = random.generateSeed(32); @@ -122,83 +118,114 @@ public KdbxHeader(int version) { innerRandomStreamKey = random.generateSeed(32); streamStartBytes = new byte[32]; protectedStreamAlgorithm = ProtectedStreamAlgorithm.SALSA_20; + + kdfparameters = Aes.createKdfParameters(); + } + + /** + * Compute the Hmac Key Digest + * KdbxFile.cs Computekeys + * + * @param credentials the credentials + * @return the digest + */ + public byte[] getHmacKey(Credentials credentials) { + // Compute the Hmac Key Digest + // KdbxFile.cs Computekeys + MessageDigest md = Encryption.getSha512MessageDigestInstance(); + md.update(getMasterSeed()); + md.update(getTransformedKeyDigest(credentials.getKey())); + return md.digest(new byte[]{1}); } + /** + * Verify the header Hmac + * + * @param key the transformed Hmac Key for the header + * @param bytes the bytes to compare to verify + */ + public void verifyHeaderHmac(byte[] key, byte[] bytes) { + Mac mac = Encryption.getHMacSha256Instance(key); + byte[] computedHmacSha256 = mac.doFinal(getHeaderBytes()); + if (!Arrays.equals(computedHmacSha256, bytes)) { + throw new IllegalStateException("Header HMAC does not match"); + } + } + + // Alternative implementation of above using bouncy castle + /* + HMac hmac = new HMac(new SHA256Digest()); + hmac.init(new KeyParameter(hmacKey64)); + hmac.update(kdbxHeader.getHeaderBytes(), 0, kdbxHeader.getHeaderBytes().length); + byte[] computedHmacSha256 = new byte[32]; + hmac.doFinal(computedHmacSha256, 0); + */ + /** * Create a decrypted input stream using supplied digest and this header * apply decryption to the passed encrypted input stream * - * @param digest the key digest + * @param digest the key digest * @param inputStream the encrypted input stream * @return a decrypted stream - * @throws IOException if something bad happens */ - public InputStream createDecryptedStream(byte[] digest, InputStream inputStream) throws IOException { + public InputStream createDecryptedStream(byte[] digest, InputStream inputStream) { // return digest of master seed and hash MessageDigest md = getSha256MessageDigestInstance(); md.update(masterSeed); byte[] finalKeyDigest = md.digest(getTransformedKeyDigest(digest)); - - if (AES_CIPHER.equals(cipherUuid)) { - return Encryption.getDecryptedInputStream(inputStream, Aes.getCipher(), finalKeyDigest, getEncryptionIv()); - } else if (CHACHA_CIPHER.equals(cipherUuid)) { - return Encryption.getDecryptedInputStream(inputStream, ChaCha.getCipher(), finalKeyDigest, getEncryptionIv()); - } - throw new UnsupportedOperationException("Unknown encryption cipher " + cipherUuid); + CipherAlgorithm ca = Encryption.Cipher.getCipherAlgorithm(cipherUuid); + return ca.getDecryptedInputStream(inputStream, finalKeyDigest, encryptionIv); } - public StreamEncryptor getInnerStreamEncryptor () { + public StreamEncryptor getInnerStreamEncryptor() { return getVersion() == 4 ? new StreamEncryptor.ChaCha20(getInnerRandomStreamKey()) : new StreamEncryptor.Salsa20(getInnerRandomStreamKey()); } + /** + * Takes the composite credentials and transforms them according to the underlying KDF algorithm. + * @param digest the credentials digested + * @return the transformed digest + */ public byte[] getTransformedKeyDigest(byte[] digest) { - byte[] transformedKeyDigest; - - UUID kdf = null; - if (kdfparameters != null) { - kdf = kdfparameters.get("$UUID").asUuid(); - } // v3 doesn't have a kdf therefore AES - if (kdf == null || Aes.KDF.equals(kdf)){ - transformedKeyDigest = Aes.getTransformedKey(digest, getTransformSeed(), getTransformRounds()); - } else if (Argon.argon2_kdf.equals(kdf)) { - transformedKeyDigest = Argon.getTransformedKey(digest, kdfparameters); - } else { - throw new UnsupportedOperationException("Unknown transform KDF " + kdf); + if (kdfparameters == null) { + return Aes.getTransformedKey(digest, transformSeed, transformRounds); } - return transformedKeyDigest; + KeyDerivationFunction kdf = Encryption.Kdf.getKdf(kdfparameters.mustGet("$UUID").asUuid()); + return kdf.getTransformedKey(digest, kdfparameters); } /** * Create an unencrypted outputstream using the supplied digest and this header * and use the supplied output stream to write encrypted data. - * @param digest the key digest + * + * @param digest the key digest * @param outputStream the output stream which is the destination for encrypted data * @return an output stream to write unencrypted data to - * @throws IOException if something bad happens */ - public OutputStream createEncryptedStream(byte[] digest, OutputStream outputStream) throws IOException { + public OutputStream createEncryptedStream(byte[] digest, OutputStream outputStream) { // return digest of master seed and hash MessageDigest md = getSha256MessageDigestInstance(); md.update(masterSeed); byte[] finalKeyDigest = md.digest(getTransformedKeyDigest(digest)); - return Encryption.getEncryptedOutputStream(outputStream, finalKeyDigest, getEncryptionIv()); + return Aes.getInstance().getEncryptedOutputStream(outputStream, finalKeyDigest, getEncryptionIv()); } public byte[] getTransformSeed() { if (version < 4) { return transformSeed; } - return kdfparameters.get(Aes.KdfKeys.ParamSeed).asByteArray(); + return kdfparameters.mustGet(Aes.KdfKeys.ParamSeed).asByteArray(); } public long getTransformRounds() { if (version < 4) { return transformRounds; } - return kdfparameters.get(Aes.KdfKeys.ParamRounds).asLong(); + return kdfparameters.mustGet(Aes.KdfKeys.ParamRounds).asLong(); } public UUID getCipherUuid() { @@ -239,10 +266,18 @@ public int getVersion() { public StreamEncryptor getStreamEncryptor() { switch (getProtectedStreamAlgorithm()) { - case NONE: {throw new IllegalStateException("Inner stream encoding of NONE");} - case ARC_FOUR: {throw new UnsupportedOperationException("Arc Four inner stream not supported");} - case SALSA_20: {return new StreamEncryptor.Salsa20(this.innerRandomStreamKey);} - case CHA_CHA_20: {return new StreamEncryptor.ChaCha20(this.innerRandomStreamKey);} + case NONE: { + throw new IllegalStateException("Inner stream encoding of NONE"); + } + case ARC_FOUR: { + throw new UnsupportedOperationException("Arc Four inner stream not supported"); + } + case SALSA_20: { + return new StreamEncryptor.Salsa20(this.innerRandomStreamKey); + } + case CHA_CHA_20: { + return new StreamEncryptor.ChaCha20(this.innerRandomStreamKey); + } } throw new IllegalStateException("Inner stream encoding unsupported"); } @@ -286,7 +321,7 @@ public void setHeaderHash(byte[] headerHash) { public void setCipherUuid(byte[] uuid) { ByteBuffer b = ByteBuffer.wrap(uuid); UUID incoming = new UUID(b.getLong(), b.getLong(8)); - if (!incoming.equals(AES_CIPHER) && !incoming.equals(CHACHA_CIPHER)) { + if (!incoming.equals(Aes.getInstance().getCipherUuid()) && !incoming.equals(ChaCha.getInstance().getCipherUuid())) { throw new IllegalStateException("Unknown Cipher UUID " + incoming.toString()); } this.cipherUuid = incoming; @@ -308,14 +343,15 @@ public void setCustomData(VariantDictionary customData) { } public void addBinary(byte[] bytes) { - + // TODO something about binaries in V4 } + public byte[] getHeaderBytes() { return headerBytes; } public void setHeaderBytes(byte[] headerBytes) { - byte [] copy = new byte[headerBytes.length]; + byte[] copy = new byte[headerBytes.length]; System.arraycopy(headerBytes, 0, copy, 0, headerBytes.length); this.headerBytes = copy; } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java index ec980053..c0a4d364 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java @@ -25,14 +25,13 @@ import org.linguafranca.pwdb.hashedblock.HmacBlockInputStream; import org.linguafranca.pwdb.security.Encryption; import org.linguafranca.pwdb.security.VariantDictionary; -import org.spongycastle.crypto.digests.SHA256Digest; -import org.spongycastle.crypto.macs.HMac; -import org.spongycastle.crypto.params.KeyParameter; import java.io.*; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.security.*; +import java.security.DigestInputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; import java.util.Arrays; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; @@ -103,9 +102,9 @@ public static InputStream createUnencryptedInputStream(Credentials credentials, if (kdbxHeader.getVersion() >= 4) { - byte[] hmacKey = verifyOuterHeader(kdbxHeader, credentials, new DataInputStream(inputStream)); + verifyOuterHeader(kdbxHeader, credentials, new DataInputStream(inputStream)); - HmacBlockInputStream hmacBlockInputStream = new HmacBlockInputStream(hmacKey, inputStream, true); + HmacBlockInputStream hmacBlockInputStream = new HmacBlockInputStream(kdbxHeader.getHmacKey(credentials), inputStream, true); plainTextStream = kdbxHeader.createDecryptedStream(credentials.getKey(), hmacBlockInputStream); @@ -155,6 +154,12 @@ public static OutputStream createEncryptedOutputStream(Credentials credentials, } + /** + * Checks that the decrypted stream starts with the expected bytes in V3 format + * @param kdbxHeader the header + * @param decryptedInputStream the decrypted stream + * @throws IOException if the stream cannot be read, etc. + */ private static void checkStartBytes(KdbxHeader kdbxHeader, InputStream decryptedInputStream) throws IOException { LittleEndianDataInputStream ledis = new LittleEndianDataInputStream(decryptedInputStream); @@ -165,6 +170,12 @@ private static void checkStartBytes(KdbxHeader kdbxHeader, InputStream decrypted } } + /** + * Writes the expected stream start bytes to the encrypted stream for V3 format + * @param kdbxHeader the header + * @param encryptedOutputStream the encypted stream + * @throws IOException if the stream cannot be written, etc. + */ private static void writeStartBytes(KdbxHeader kdbxHeader, OutputStream encryptedOutputStream) throws IOException { LittleEndianDataOutputStream ledos = new LittleEndianDataOutputStream(encryptedOutputStream); ledos.write(kdbxHeader.getStreamStartBytes()); @@ -241,42 +252,24 @@ public static KdbxHeader readOuterHeader(InputStream inputStream, KdbxHeader kdb /** * V4 header is followed by an SHA256 and then contains an HMACSHA256 after that. * @param kdbxHeader the header containing the relevant parameters - * @param credentials + * @param credentials the credentials - used to verify the HMAC * @param input an input source - * @return Hmac Key Digest for use in the HmacBlockInputStream * @throws IOException on error */ - public static byte[] verifyOuterHeader(KdbxHeader kdbxHeader, Credentials credentials, DataInput input) throws IOException { + public static void verifyOuterHeader(KdbxHeader kdbxHeader, Credentials credentials, DataInput input) throws IOException { // check the SHA byte [] sha256 = getBytes(32, input); if (!Arrays.equals(kdbxHeader.getHeaderHash(), sha256)) { throw new IllegalStateException("Header hash does not match"); } - // Compute the Hmac Key Digest - // KdbxFile.cs Computekeys - MessageDigest md = Encryption.getSha512MessageDigestInstance(); - md.update(kdbxHeader.getMasterSeed()); - md.update(kdbxHeader.getTransformedKeyDigest(credentials.getKey())); - byte [] hmacKey = md.digest(new byte [] {1}); + byte[] hmacKey = kdbxHeader.getHmacKey(credentials); // get the key for the header Hmac (using sequence number -1) // KdbxFile.cs ComputeHeaderHmac - byte [] hmacKey64 = HmacBlockInputStream.getHmacBlockKey(hmacKey, -1L, ByteOrder.LITTLE_ENDIAN); + byte [] hmacKey64 = Encryption.transformHmacKey(hmacKey, Helpers.toBytes(-1L, ByteOrder.LITTLE_ENDIAN)); - HMac hmac = new HMac(new SHA256Digest()); - hmac.init(new KeyParameter(hmacKey64)); - hmac.update(kdbxHeader.getHeaderBytes(), 0, kdbxHeader.getHeaderBytes().length); - byte[] computedHmacSha256 = new byte[32]; - hmac.doFinal(computedHmacSha256, 0); - - // check for correctness - byte [] storedHmacSha256 = getBytes(32, input); - - if (!Arrays.equals(computedHmacSha256, storedHmacSha256)) { - throw new IllegalStateException("Header HMAC does not match"); - } - return hmacKey; + kdbxHeader.verifyHeaderHmac(hmacKey64, getBytes(32, input)); } @@ -412,10 +405,7 @@ private static VariantDictionary makeVariantDictionary(int length, DataInput inp buf.order(ByteOrder.LITTLE_ENDIAN); // version number must be 0x01?? - VariantDictionary vd = new VariantDictionary(buf.getShort()); - if ((vd.getVersion() & 0xFF00) != 0x0100) { - throw new IllegalStateException("Variant dictionary must have version 0x0100"); - } + VariantDictionary vd = new VariantDictionary((short) (buf.getShort() >> 8)); // sequence of entries followed by a byte 0 byte type = buf.get(); @@ -431,7 +421,7 @@ private static VariantDictionary makeVariantDictionary(int length, DataInput inp buf.get(value); // add entry - vd.put(new String(key), type, value); + vd.put(new String(key), VariantDictionary.EntryType.get(type), value); type = buf.get(); } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java index 5691ae62..f9d9b587 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java @@ -26,13 +26,25 @@ /** * This class implements KDBX formatted saving and loading of databases * - * @author jo */ public class KdbxStreamFormat implements StreamFormat { private final Version version; - public static enum Version {KDBX31, KDBX4} + public enum Version { + KDBX31(3), + KDBX4(4); + + private final int version; + + Version(int num) { + this.version = num; + } + + int getVersionNum() { + return this.version; + } + } /** * Create a StreamFormat for reading or for writing v3 @@ -64,7 +76,7 @@ public void load(SerializableDatabase serializableDatabase, Credentials credenti @Override public void save(SerializableDatabase serializableDatabase, Credentials credentials, OutputStream encryptedOutputStream) throws IOException { // fresh kdbx header - KdbxHeader kdbxHeader = new KdbxHeader(4); + KdbxHeader kdbxHeader = new KdbxHeader(version.getVersionNum()); OutputStream unencrytedOutputStream = KdbxSerializer.createEncryptedOutputStream(credentials, kdbxHeader, encryptedOutputStream); if (version == Version.KDBX31) { serializableDatabase.setHeaderHash(kdbxHeader.getHeaderHash()); diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java index ec33fe6e..1a8adf3d 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java @@ -29,8 +29,9 @@ public void getHmacStream() throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); System.out.println("Version " + header.getVersion()); - byte [] hmacKey = KdbxSerializer.verifyOuterHeader(header, new KdbxCredentials.Password("123".getBytes()), new LittleEndianDataInputStream(inputStream)); - HmacBlockInputStream hmacBlockInputStream = new HmacBlockInputStream(hmacKey, inputStream, true); + KdbxCreds creds = new KdbxCreds("123".getBytes()); + KdbxSerializer.verifyOuterHeader(header, creds, new LittleEndianDataInputStream(inputStream)); + HmacBlockInputStream hmacBlockInputStream = new HmacBlockInputStream(header.getHmacKey(creds), inputStream, true); byte [] buf = new byte [1024]; int bytesRead; while ((bytesRead = hmacBlockInputStream.read(buf)) != -1) { diff --git a/readme.md b/readme.md index ab19c762..f4eafdf5 100644 --- a/readme.md +++ b/readme.md @@ -122,7 +122,10 @@ Hence the creation of abstract Database interfaces rather than following the Kee exactly. KeePass is in effect defined by the code that Dominik writes to create and maintain the project. -Hence there is no definitive specification of KeePass files other than that code. For the sake of +Hence there is not much by way of definitive specification of KeePass files other than that code. There +is [a discussion](https://keepass.info/help/kb/kdbx_4.html) of the differences between KDBX version 3.1 and version 4. + +For the sake of clarification and my own satisfaction I have written about my understanding of KeePass formats in the following locations: 1. The Javadoc header to [KdbxSerializer](http://javadoc.io/page/org.linguafranca.pwdb/KeePassJava2-kdbx/latest/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.html) describes KDBX stream formatting. diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java index 3fea86c5..4446ab2e 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java @@ -16,6 +16,7 @@ package org.linguafranca.pwdb.kdbx.simple; +import org.jetbrains.annotations.NotNull; import org.linguafranca.pwdb.base.AbstractEntry; import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.simple.converter.UuidConverter; @@ -216,7 +217,7 @@ public SimpleGroup getParent() { } @Override - public UUID getUuid() { + public @NotNull UUID getUuid() { return uuid; } From 9ae42fa2b4317fdc055c88fcbe1c92216b01d19e Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 13 Feb 2018 13:53:12 +0000 Subject: [PATCH 073/228] Further Javadoc etc amends --- .../java/org/linguafranca/pwdb/Group.java | 49 ++++++++++++++---- .../pwdb/base/AbstractDatabase.java | 9 +++- .../linguafranca/pwdb/base/AbstractGroup.java | 47 +++++++++-------- .../pwdb/security/VariantDictionary.java | 21 ++++---- .../pwdb/kdbx/dom/DomDatabaseWrapper.java | 8 ++- .../pwdb/kdbx/dom/DomGroupWrapper.java | 2 + ...Test.java => DomBinaryPropertyV3Test.java} | 4 +- .../pwdb/kdbx/jaxb/JaxbGroup.java | 2 + .../org/linguafranca/pwdb/kdb/KdbGroup.java | 2 + .../pwdb/kdbx/simple/SimpleGroup.java | 2 + .../pwdb/checks/BinaryPropertyChecks.java | 51 ++++++++++++++++++- 11 files changed, 150 insertions(+), 47 deletions(-) rename dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/{DomEntryWrapperTest.java => DomBinaryPropertyV3Test.java} (91%) diff --git a/database/src/main/java/org/linguafranca/pwdb/Group.java b/database/src/main/java/org/linguafranca/pwdb/Group.java index 94bfae55..152a3f12 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Group.java +++ b/database/src/main/java/org/linguafranca/pwdb/Group.java @@ -16,13 +16,20 @@ package org.linguafranca.pwdb; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; import java.util.List; import java.util.UUID; /** - * Interface for a Database Group. Databases have exactly one Root Group. - * Further Groups of the database are help as sub-Groups of other Groups. - * A Database may contain Groups to an indefinite level. + * Interface for a Database Group. + * + *

    Databases have exactly one Root Group. + * + *

    Other Groups of the database are a child of another Group. + * + *

    A Database may contain Groups to an indefinite level. * *

    Any Group may contain {@link Entry} items. * @@ -47,7 +54,7 @@ public interface Group , G extends Group, G extends GroupEntry match is described under {@link Entry#match(String)} * + *

    Note: finding within recycle bin is supported, recusion into recycle bin is inhibited + * * @param match the text to match * @param recursive whether to include sub groups in the process * @return a modifiable-by-caller list @@ -137,14 +146,16 @@ public interface Group , G extends GroupEntry match is described under {@link Entry#match(String)} + *

    Entry match is described under {@link Entry#match(Entry.Matcher)} + * + *

    Note: finding within recycle bin is supported, recusion into recycle bin is inhibited * * @param matcher the mathcher to use * @param recursive whether to include sub groups in the process * @return a modifiable-by-caller list - * @see Entry#match(String) + * @see Entry#match(Entry.Matcher) */ List findEntries(Entry.Matcher matcher, boolean recursive); @@ -165,7 +176,7 @@ public interface Group , G extends Group parent); @@ -176,15 +187,33 @@ public interface Group , G extends Group groups = new ArrayList<>(); visit(new Visitor.Default() { - // ignore sub groups of the recycle bin + // set to true while visiting sub groups of recycle bin boolean recycle; @Override public void startVisit(Group group) { if (!recycle && group.getUuid().equals(uuid)) { + //noinspection unchecked groups.add((G) group); } if (group.isRecycleBin()) { @@ -197,8 +201,11 @@ public boolean deleteGroup(final UUID uuid) { if (g==null) { return false; } + + //noinspection ConstantConditions g.getParent().removeGroup(g); if (isRecycleBinEnabled()) { + //noinspection ConstantConditions getRecycleBin().addGroup(g); } return true; diff --git a/database/src/main/java/org/linguafranca/pwdb/base/AbstractGroup.java b/database/src/main/java/org/linguafranca/pwdb/base/AbstractGroup.java index 727ea7bc..516577e8 100644 --- a/database/src/main/java/org/linguafranca/pwdb/base/AbstractGroup.java +++ b/database/src/main/java/org/linguafranca/pwdb/base/AbstractGroup.java @@ -43,31 +43,32 @@ public List findGroups(String group1) { return result; } - @Override - public List findEntries(String find, boolean recursive) { - List result = new ArrayList<>(getEntries().size()); - if (isRecycleBin()) { - return result; - } - for (E entry: getEntries()){ - if (entry.match(find)){ - result.add(entry); - } + /** + * Local helper class to avoid violating DRY in {@link AbstractGroup#findEntries(String, boolean)} + */ + private static class TextMatcher implements Entry.Matcher { + + private final String text; + + private TextMatcher(String text) { + this.text = text; } - if (recursive) { - for (G group : getGroups()) { - result.addAll(group.findEntries(find, true)); - } + + @Override + public boolean matches(Entry entry) { + return entry.match(text); } - return result; + } + + + @Override + public List findEntries(String find, boolean recursive) { + return findEntries(new TextMatcher(find), recursive); } @Override public List findEntries(Entry.Matcher matcher, boolean recursive) { List result = new ArrayList<>(getEntries().size()); - if (isRecycleBin()) { - return result; - } for (E entry: getEntries()){ if (entry.match(matcher)){ result.add(entry); @@ -75,6 +76,10 @@ public List findEntries(Entry.Matcher matcher, boolean recursive) { } if (recursive) { for (G group : getGroups()) { + // don't recurse into recycle bin + if (group.isRecycleBin()) { + continue; + } result.addAll(group.findEntries(matcher, true)); } } @@ -100,11 +105,11 @@ public String getPath() { while ((parent=parent.getParent()) != null) { parents.push(parent); } - String result = "/"; + StringBuilder result = new StringBuilder("/"); while (parents.size() > 0) { - result = result + parents.pop().getName() + "/"; + result.append(parents.pop().getName()).append("/"); } - return result; + return result.toString(); } @Override diff --git a/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java b/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java index 318e95b8..629cac52 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java @@ -8,9 +8,9 @@ import java.nio.ByteOrder; import java.util.HashMap; import java.util.Map; -import java.util.Objects; import java.util.UUID; +import static com.google.common.base.Preconditions.checkNotNull; import static org.linguafranca.pwdb.security.VariantDictionary.EntryType.ARRRAY; import static org.linguafranca.pwdb.security.VariantDictionary.EntryType.UINT64; @@ -23,8 +23,11 @@ @SuppressWarnings("WeakerAccess") public class VariantDictionary { - private short version; - private Map entries = new HashMap<>(); + private final short version; + private final Map entries = new HashMap<>(); + + private final static String knn = "VariantDictionary key must not be null"; + private final static String vnn = "VariantDictionary.Entry value must not be null"; /** * The list of permissible entry types @@ -69,7 +72,7 @@ public Entry(EntryType entryType, @NotNull byte[] value) { public Entry(EntryType entryType, @NotNull byte[] value, ByteOrder byteOrder) { this.type = entryType.value; - this.value = Objects.requireNonNull(value); + this.value = checkNotNull(value, vnn); this.byteOrder = byteOrder; } @@ -165,8 +168,8 @@ public short getVersion() { * @param type the data type of the entry * @param value a buffer containing an appropriate entry */ - public void put(@NotNull String key, EntryType type, byte[] value) { - entries.put(Objects.requireNonNull(key), new Entry(type, value)); + public void put(@NotNull String key, EntryType type, @NotNull byte[] value) { + entries.put(checkNotNull(key), new Entry(type, checkNotNull(value))); } /** @@ -177,14 +180,14 @@ public void putUuid(@NotNull String key, UUID uuid) { ByteBuffer bb = ByteBuffer.wrap(buf); bb.putLong(0, uuid.getMostSignificantBits()); bb.putLong(8, uuid.getLeastSignificantBits()); - entries.put(Objects.requireNonNull(key), new Entry(ARRRAY, buf)); + entries.put(checkNotNull(key, knn), new Entry(ARRRAY, buf)); } /** * Put a byte array under the key defined */ public void putByteArray(@NotNull String key, @NotNull byte[] value) { - entries.put(Objects.requireNonNull(key), new Entry(ARRRAY, value)); + entries.put(checkNotNull(key, knn), new Entry(ARRRAY, value)); } /** @@ -194,6 +197,6 @@ public void putLong(@NotNull String key, long value) { byte[] buf = new byte[8]; ByteBuffer bb = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN); bb.putLong(value); - entries.put(Objects.requireNonNull(key), new Entry(UINT64, buf)); + entries.put(checkNotNull(key, knn), new Entry(UINT64, buf)); } } diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java index 5f0b7a68..70e4b236 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java @@ -16,6 +16,7 @@ package org.linguafranca.pwdb.kdbx.dom; +import org.jetbrains.annotations.NotNull; import org.linguafranca.pwdb.base.AbstractDatabase; import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; @@ -31,6 +32,7 @@ import java.io.OutputStream; import java.util.UUID; +import static com.google.common.base.Preconditions.checkNotNull; import static org.linguafranca.pwdb.kdbx.Helpers.base64FromUuid; import static org.linguafranca.pwdb.kdbx.dom.DomHelper.*; @@ -57,8 +59,10 @@ public DomDatabaseWrapper (StreamFormat streamFormat, Credentials credentials, I init(); } - public static DomDatabaseWrapper load (Credentials credentials, InputStream inputStream) throws IOException { - return new DomDatabaseWrapper(new KdbxStreamFormat(), credentials, inputStream); + public static DomDatabaseWrapper load (@NotNull Credentials credentials, @NotNull InputStream inputStream) throws IOException { + return new DomDatabaseWrapper(new KdbxStreamFormat(), + checkNotNull(credentials, "Credentials must not be null"), + checkNotNull(inputStream, "InputStream must not be null")); } private void init() { diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomGroupWrapper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomGroupWrapper.java index b4324f86..504cf848 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomGroupWrapper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomGroupWrapper.java @@ -16,6 +16,7 @@ package org.linguafranca.pwdb.kdbx.dom; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.linguafranca.pwdb.Icon; import org.linguafranca.pwdb.base.AbstractGroup; @@ -194,6 +195,7 @@ public void setIcon(DomIconWrapper icon) { database.setDirty(true); } + @NotNull @Override public DomDatabaseWrapper getDatabase() { return database; diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapperTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV3Test.java similarity index 91% rename from dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapperTest.java rename to dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV3Test.java index c2b776dc..7c4b8788 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapperTest.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV3Test.java @@ -28,9 +28,9 @@ /** * @author jo */ -public class DomEntryWrapperTest extends BinaryPropertyChecks { +public class DomBinaryPropertyV3Test extends BinaryPropertyChecks { - public DomEntryWrapperTest () throws IOException { + public DomBinaryPropertyV3Test() throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment.kdbx"); database = DomDatabaseWrapper.load(new KdbxCreds("123".getBytes()), inputStream); } diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbGroup.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbGroup.java index 1d758fa7..1ceddb9e 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbGroup.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbGroup.java @@ -16,6 +16,7 @@ package org.linguafranca.pwdb.kdbx.jaxb; +import org.jetbrains.annotations.NotNull; import org.linguafranca.pwdb.base.AbstractGroup; import org.linguafranca.pwdb.kdbx.jaxb.binding.JaxbEntryBinding; import org.linguafranca.pwdb.kdbx.jaxb.binding.JaxbGroupBinding; @@ -201,6 +202,7 @@ public void setIcon(JaxbIcon icon) { this.delegate.setIconID(icon.getIndex()); } + @NotNull @Override public JaxbDatabase getDatabase() { return database; diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbGroup.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbGroup.java index 21e6a6ee..3df785cb 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbGroup.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbGroup.java @@ -16,6 +16,7 @@ package org.linguafranca.pwdb.kdb; +import org.jetbrains.annotations.NotNull; import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.Group; import org.linguafranca.pwdb.Icon; @@ -209,6 +210,7 @@ public String toString() { return getPath() + String.format(" (%s) %s [%d]", uuid.toString(), time, flags); } + @NotNull public KdbDatabase getDatabase() { return database; } diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java index 703b3e26..4a6235dc 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java @@ -16,6 +16,7 @@ package org.linguafranca.pwdb.kdbx.simple; +import org.jetbrains.annotations.NotNull; import org.linguafranca.pwdb.Group; import org.linguafranca.pwdb.kdbx.simple.converter.KeePassBooleanConverter; import org.linguafranca.pwdb.kdbx.simple.converter.UuidConverter; @@ -227,6 +228,7 @@ public void setIcon(SimpleIcon icon) { touch(); } + @NotNull @Override public SimpleDatabase getDatabase() { return database; diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java index bb0adc03..b8b8bd91 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java @@ -19,6 +19,7 @@ import com.google.common.io.ByteStreams; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.Database; @@ -31,15 +32,35 @@ import static org.junit.Assert.*; - /** + * Tests to verify that binary properties work correctly. Resources associated with this module contain files + * named "Attachment*.kdbx" which contain two entries: + * + *

    + *
    "Test Attachment"
    + *
    Contains an attachment "Letter J" which is also present in the resources directory
    + *
    "Test 2 Attachment"
    + *
    Contains an attachment "Letter L" which is also present in the resources directory
    + *
    + * + * When used as a test suite for a concrete implementation, subclass and name the class *Test etc to conform + * with Junit rules + *

    + * Subclasses should test both V3 KDBX files (Attachment.kdbx) and V4 (Attachment-ChaCha20-Argon2.kdbx) since + * attachments are handled differently in the two versions. + * * @author jo */ public abstract class BinaryPropertyChecks { + public Database database; + @SuppressWarnings("unused") public abstract void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException; + /** + * Retrieve and verify attachment "letter J" + */ @Test public void getBinaryProperty() throws Exception { Entry entry = database.findEntries("Test attachment").get(0); @@ -49,6 +70,9 @@ public void getBinaryProperty() throws Exception { Assert.assertArrayEquals(original, letterJ); } + /** + * Retrieve and verify attachment "letter L" + */ @Test public void getAnotherBinaryProperty() throws Exception { Entry entry = database.findEntries("Test 2 attachment").get(0); @@ -58,6 +82,9 @@ public void getAnotherBinaryProperty() throws Exception { Assert.assertArrayEquals(original, letterL); } + /** + * Add the Letter L to the entry containing letter J + */ @Test public void setBinaryProperty() throws Exception { InputStream testfile = getClass().getClassLoader().getResourceAsStream("letter L.jpeg"); @@ -70,8 +97,11 @@ public void setBinaryProperty() throws Exception { Assert.assertTrue(database.isDirty()); } + /** + * Verify that the entries have the right attachments + */ @Test - public void getBinaryPropertyNames() throws Exception { + public void getBinaryPropertyNames() { Entry entry = database.findEntries("Test attachment").get(0); assertArrayEquals(new String[] {"letter J.jpeg"}, entry.getBinaryPropertyNames().toArray()); @@ -79,11 +109,18 @@ public void getBinaryPropertyNames() throws Exception { assertArrayEquals(new String[] {"letter J.jpeg", "letter L.jpeg"}, entry.getBinaryPropertyNames().toArray()); } + /** + * Verify that the database correctly reports that its supports binary attachments - override for + * databases that don't + */ @Test public void checkSupported(){ assertTrue(database.supportsBinaryProperties()); } + /** + * Verify that the binary properties can be added and removed correctly + */ @Test public void checkAddChangeRemoveBinaryProperty() { byte[] test = new byte[] {0, 1, 2 ,3}; @@ -103,4 +140,14 @@ public void checkAddChangeRemoveBinaryProperty() { // same number of properties as we started with assertEquals(1, entry.getBinaryPropertyNames().size()); } + + /** + * Checks that a database with binary properties saves and reloads correctly + */ + @Test @Ignore + public void saveAndReloadCheck() { + // TODO + } + + } From 73b6783f06cda0e0bfaf63d6220da98bc610e1d6 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Wed, 14 Feb 2018 13:58:10 +0000 Subject: [PATCH 074/228] Correcting times, adding V4 binary property load --- .../linguafranca/pwdb/base/AbstractGroup.java | 30 +++---- .../pwdb/kdbx/dom/DomEntryWrapper.java | 29 ++----- .../linguafranca/pwdb/kdbx/dom/DomHelper.java | 30 ++++--- .../kdbx/dom/DomSerializableDatabase.java | 30 +++++-- .../kdbx/dom/DomBinaryPropertyV4Test.java | 42 ++++++++++ .../pwdb/kdbx/jaxb/JaxbDatabase.java | 6 ++ .../pwdb/kdbx/jaxb/JaxbEntry.java | 7 +- .../kdbx/jaxb/JaxbSerializableDatabase.java | 23 ++++- ...est.java => JaxbBinaryPropertyV3Test.java} | 4 +- .../kdbx/jaxb/JaxbBinaryPropertyV4Test.java | 44 ++++++++++ .../org/linguafranca/pwdb/kdbx/Helpers.java | 2 +- .../linguafranca/pwdb/kdbx/KdbxHeader.java | 68 ++++++++++++--- .../pwdb/kdbx/KdbxSerializer.java | 5 +- .../pwdb/kdbx/KdbxStreamFormat.java | 7 ++ .../pwdb/kdbx/SerializableDatabase.java | 2 + .../pwdb/kdbx/simple/SimpleDatabase.java | 28 ++++++- .../pwdb/kdbx/simple/SimpleEntry.java | 13 +-- .../pwdb/kdbx/simple/model/KeePassFile.java | 79 +++++++++--------- ...t.java => SimpleBinaryPropertyV3Test.java} | 4 +- .../simple/SimpleBinaryPropertyV4Test.java | 44 ++++++++++ .../pwdb/checks/DatabaseLoaderChecks.java | 7 +- .../resources/Attachment-ChaCha20-Argon2.kdbx | Bin 0 -> 9842 bytes test/src/main/resources/test123-AES-AES.kdbx | Bin 0 -> 3486 bytes .../main/resources/test123-AES-Argon2.kdbx | Bin 0 -> 3509 bytes .../main/resources/test123-ChaCha20-AES.kdbx | Bin 0 -> 3470 bytes .../resources/test123-ChaCha20-Argon2.kdbx | Bin 0 -> 3514 bytes 26 files changed, 367 insertions(+), 137 deletions(-) create mode 100644 dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV4Test.java rename jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/{JaxbEntryTest.java => JaxbBinaryPropertyV3Test.java} (92%) create mode 100644 jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.java rename simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/{SimpleEntryTest.java => SimpleBinaryPropertyV3Test.java} (92%) create mode 100644 simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java create mode 100755 test/src/main/resources/Attachment-ChaCha20-Argon2.kdbx create mode 100755 test/src/main/resources/test123-AES-AES.kdbx create mode 100755 test/src/main/resources/test123-AES-Argon2.kdbx create mode 100755 test/src/main/resources/test123-ChaCha20-AES.kdbx create mode 100755 test/src/main/resources/test123-ChaCha20-Argon2.kdbx diff --git a/database/src/main/java/org/linguafranca/pwdb/base/AbstractGroup.java b/database/src/main/java/org/linguafranca/pwdb/base/AbstractGroup.java index 516577e8..cf13c0af 100644 --- a/database/src/main/java/org/linguafranca/pwdb/base/AbstractGroup.java +++ b/database/src/main/java/org/linguafranca/pwdb/base/AbstractGroup.java @@ -43,26 +43,26 @@ public List findGroups(String group1) { return result; } - /** - * Local helper class to avoid violating DRY in {@link AbstractGroup#findEntries(String, boolean)} - */ - private static class TextMatcher implements Entry.Matcher { + @Override + public List findEntries(String find, boolean recursive) { + /* + * Local helper class to avoid violating DRY in {@link AbstractGroup#findEntries(String, boolean)}. + * Would-be lambda in Java 8. + */ + class TextMatcher implements Entry.Matcher { - private final String text; + private final String text; - private TextMatcher(String text) { - this.text = text; - } + private TextMatcher(String text) { + this.text = text; + } - @Override - public boolean matches(Entry entry) { - return entry.match(text); + @Override + public boolean matches(Entry entry) { + return entry.match(text); + } } - } - - @Override - public List findEntries(String find, boolean recursive) { return findEntries(new TextMatcher(find), recursive); } diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java index 16d8949e..39575301 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java @@ -20,7 +20,6 @@ import org.linguafranca.pwdb.kdbx.Helpers; import org.w3c.dom.Element; -import java.text.ParseException; import java.util.*; /** @@ -173,20 +172,12 @@ public void setIcon(DomIconWrapper icon) { @Override public Date getLastAccessTime() { - try { - return DomHelper.dateFormatter.parse(DomHelper.getElementContent(DomHelper.LAST_ACCESS_TIME_ELEMENT_NAME, element)); - } catch (ParseException e) { - return new Date(0); - } + return Helpers.toDate(DomHelper.getElementContent(DomHelper.LAST_ACCESS_TIME_ELEMENT_NAME, element)); } @Override public Date getCreationTime() { - try { - return DomHelper.dateFormatter.parse(DomHelper.getElementContent(DomHelper.CREATION_TIME_ELEMENT_NAME, element)); - } catch (ParseException e) { - return new Date(0); - } + return Helpers.toDate(DomHelper.getElementContent(DomHelper.CREATION_TIME_ELEMENT_NAME, element)); } @Override @@ -202,32 +193,24 @@ public void setExpires(boolean expires) { @Override public Date getExpiryTime() { - try { - return DomHelper.dateFormatter.parse(DomHelper.getElementContent(DomHelper.EXPIRY_TIME_ELEMENT_NAME, element)); - } catch (ParseException e) { - return new Date(0); - } + return Helpers.toDate(DomHelper.getElementContent(DomHelper.EXPIRY_TIME_ELEMENT_NAME, element)); } @Override public void setExpiryTime(Date expiryTime) throws IllegalArgumentException { if (expiryTime == null) throw new IllegalArgumentException("expiryTime may not be null"); - String formatted = DomHelper.dateFormatter.format(expiryTime); + String formatted = Helpers.fromDate(expiryTime); DomHelper.setElementContent(DomHelper.EXPIRY_TIME_ELEMENT_NAME, element, formatted); } @Override public Date getLastModificationTime() { - try { - return DomHelper.dateFormatter.parse(DomHelper.getElementContent(DomHelper.LAST_MODIFICATION_TIME_ELEMENT_NAME, element)); - } catch (ParseException e) { - return new Date(0); - } + return Helpers.toDate(DomHelper.getElementContent(DomHelper.LAST_MODIFICATION_TIME_ELEMENT_NAME, element)); } @Override protected void touch() { - DomHelper.setElementContent(DomHelper.LAST_MODIFICATION_TIME_ELEMENT_NAME, element, DomHelper.dateFormatter.format(new Date())); + DomHelper.setElementContent(DomHelper.LAST_MODIFICATION_TIME_ELEMENT_NAME, element, Helpers.fromDate(new Date())); } @Override diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java index 51d8da96..12ded6c5 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java @@ -41,7 +41,7 @@ class DomHelper { static XPath xpath = XPathFactory.newInstance().newXPath(); - static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); +// static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"); static final String GROUP_ELEMENT_NAME = "Group"; static final String ENTRY_ELEMENT_NAME = "Entry"; @@ -88,7 +88,7 @@ public String getValue() { static class DateValueCreator implements ValueCreator { @Override public String getValue() { - return dateFormatter.format(new Date()); + return Helpers.fromDate(new Date()); } } @@ -205,14 +205,7 @@ static Element setBinaryElementContent(String elementPath, Element parentElement String max = xpath.evaluate("//Binaries/Binary/@ID[not(. < ../../Binary/@ID)][1]", parentElement.getOwnerDocument().getDocumentElement()); Integer newIndex = Integer.valueOf(max) + 1; - Element binaries = getElement("//Binaries", parentElement.getOwnerDocument().getDocumentElement(),false); - if (binaries == null) { - throw new IllegalStateException("Binaries not found"); - } - Element binary = (Element) binaries.appendChild(binaries.getOwnerDocument().createElement("Binary")); - binary.setTextContent(b64); - binary.setAttribute("Compressed", "True"); - binary.setAttribute("ID", newIndex.toString()); + addBinary(parentElement.getOwnerDocument().getDocumentElement(), b64, newIndex); Element result = getElement(elementPath, parentElement, true); result.setAttribute("Ref", newIndex.toString()); @@ -225,9 +218,24 @@ static Element setBinaryElementContent(String elementPath, Element parentElement } } + /** + * Add a binary property value to the V3 Meta/Binaries element + * + * @param documentElement the document element + * @param b64 a base64 gzipped encoded representation of the binary content + * @param index the index by which it is known + */ + public static void addBinary(Element documentElement, String b64, Integer index) { + Element binaries = getElement("Meta/Binaries", documentElement,true); + Element binary = (Element) binaries.appendChild(binaries.getOwnerDocument().createElement("Binary")); + binary.setTextContent(b64); + binary.setAttribute("Compressed", "True"); + binary.setAttribute("ID", index.toString()); + } + @NotNull static Element touchElement(String elementPath, Element parentElement) { - return setElementContent(elementPath, parentElement, dateFormatter.format(new Date())); + return setElementContent(elementPath, parentElement, Helpers.fromDate(new Date())); } private static Element createHierarchically(String elementPath, Element startElement) { diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java index d50933bc..4ababed0 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java @@ -101,14 +101,6 @@ public SerializableDatabase load(InputStream inputStream) throws IOException { element.removeAttribute("Protected"); } - // we need to convert all V4 dates - we'll convert them all anyway TODO find a way of not converting V3 dates - // finding all elements name ending Changed and Time - NodeList dateContent = (NodeList) DomHelper.xpath.evaluate("//*[substring(local-name(), string-length(local-name()) -3) = 'Time']", doc, XPathConstants.NODESET); - processDates(dateContent); - - dateContent = (NodeList) DomHelper.xpath.evaluate("//*[substring(local-name(), string-length(local-name()) -6) = 'Changed']", doc, XPathConstants.NODESET); - processDates(dateContent); - return this; } catch (ParserConfigurationException e) { @@ -120,6 +112,27 @@ public SerializableDatabase load(InputStream inputStream) throws IOException { } } +/* + public void dateConvert() { + try { + // finding all elements name ending Changed and Time + NodeList dateContent = (NodeList) DomHelper.xpath.evaluate("//*[substring(local-name(), string-length(local-name()) -3) = 'Time']", doc, XPathConstants.NODESET); + processDates(dateContent); + + dateContent = (NodeList) DomHelper.xpath.evaluate("//*[substring(local-name(), string-length(local-name()) -6) = 'Changed']", doc, XPathConstants.NODESET); + processDates(dateContent); + } catch (XPathExpressionException e) { + throw new IllegalStateException(e); + } + } +*/ + + @Override + public void addBinary(int index, byte[] payload) { + DomHelper.addBinary(doc.getDocumentElement(), Helpers.encodeBase64Content(payload, true),index); + } + +/* private void processDates(NodeList dateContent) { Date now = new Date(); for (int i = 0; i < dateContent.getLength(); i++){ @@ -130,6 +143,7 @@ private void processDates(NodeList dateContent) { } } +*/ @Override public void save(OutputStream outputStream) { Document copyDoc = (Document) doc.cloneNode(true); diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV4Test.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV4Test.java new file mode 100644 index 00000000..bae3ff76 --- /dev/null +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV4Test.java @@ -0,0 +1,42 @@ +/* + * Copyright 2015 Jo Rabin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx.dom; + +import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.checks.BinaryPropertyChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * @author jo + */ +public class DomBinaryPropertyV4Test extends BinaryPropertyChecks { + + public DomBinaryPropertyV4Test() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment-ChaCha20-Argon2.kdbx"); + database = DomDatabaseWrapper.load(new KdbxCreds("123".getBytes()), inputStream); + } + + @Override + public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { + database.save(credentials, outputStream); + } +} \ No newline at end of file diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java index 3b34f151..53d17e0a 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java @@ -19,8 +19,10 @@ import org.jetbrains.annotations.NotNull; import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.base.AbstractDatabase; +import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import org.linguafranca.pwdb.kdbx.StreamFormat; +import org.linguafranca.pwdb.kdbx.jaxb.binding.Binaries; import org.linguafranca.pwdb.kdbx.jaxb.binding.KeePassFile; import org.linguafranca.pwdb.kdbx.jaxb.binding.ObjectFactory; @@ -183,4 +185,8 @@ public KeePassFile getKeePassFile() { ObjectFactory getObjectFactory() { return objectFactory; } + + public void createBinary(byte[] value, Integer index) { + JaxbSerializableDatabase.addBinary(getKeePassFile(), getObjectFactory(), index, value); + } } diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java index 533193ab..1eafe10d 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java @@ -175,12 +175,7 @@ public void setBinaryProperty(String name, byte[] value) { } max++; - // create a new binary to put in the store - Binaries.Binary newBin = database.getObjectFactory().createBinariesBinary(); - newBin.setID(max); - newBin.setValue(Helpers.zipBinaryContent(value)); - newBin.setCompressed(true); - binaryList.add(newBin); + database.createBinary(value, max); // make a reference to it from the entry BinaryField binaryField = database.getObjectFactory().createBinaryField(); diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java index e80dc944..d93bf99c 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java @@ -17,12 +17,10 @@ package org.linguafranca.pwdb.kdbx.jaxb; import org.apache.commons.codec.binary.Base64; +import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.SerializableDatabase; import org.linguafranca.pwdb.kdbx.StreamEncryptor; -import org.linguafranca.pwdb.kdbx.jaxb.binding.JaxbEntryBinding; -import org.linguafranca.pwdb.kdbx.jaxb.binding.JaxbGroupBinding; -import org.linguafranca.pwdb.kdbx.jaxb.binding.KeePassFile; -import org.linguafranca.pwdb.kdbx.jaxb.binding.StringField; +import org.linguafranca.pwdb.kdbx.jaxb.binding.*; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; @@ -43,6 +41,7 @@ public class JaxbSerializableDatabase implements SerializableDatabase { protected KeePassFile keePassFile; private StreamEncryptor encryption; + private ObjectFactory objectFactory = new ObjectFactory(); @Override @@ -147,6 +146,22 @@ public void setHeaderHash(byte[] hash) { keePassFile.getMeta().setHeaderHash(hash); } + @Override + public void addBinary(int index, byte[] value) { + addBinary(keePassFile, objectFactory, index, value); + } + + public static void addBinary(KeePassFile keePassFile, ObjectFactory objectFactory, int index, byte[] value) { + // create a new binary to put in the store + Binaries.Binary newBin = objectFactory.createBinariesBinary(); + newBin.setID(index); + newBin.setValue(Helpers.zipBinaryContent(value)); + newBin.setCompressed(true); + if (keePassFile.getMeta().getBinaries() == null) { + keePassFile.getMeta().setBinaries(objectFactory.createBinaries()); + } + keePassFile.getMeta().getBinaries().getBinary().add(newBin); + } public KeePassFile getKeePassFile() { return keePassFile; diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntryTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV3Test.java similarity index 92% rename from jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntryTest.java rename to jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV3Test.java index f967b8f2..dd87d4c4 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntryTest.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV3Test.java @@ -28,9 +28,9 @@ /** * @author jo */ -public class JaxbEntryTest extends BinaryPropertyChecks { +public class JaxbBinaryPropertyV3Test extends BinaryPropertyChecks { - public JaxbEntryTest() { + public JaxbBinaryPropertyV3Test() { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment.kdbx"); database = JaxbDatabase.load(new KdbxCreds("123".getBytes()),inputStream); } diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.java new file mode 100644 index 00000000..940707b6 --- /dev/null +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015 Jo Rabin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx.jaxb; + +import org.junit.Ignore; +import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.checks.BinaryPropertyChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * @author jo + */ +@Ignore +public class JaxbBinaryPropertyV4Test extends BinaryPropertyChecks { + + public JaxbBinaryPropertyV4Test() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment-ChaCha20-Argon2.kdbx"); + database = JaxbDatabase.load(new KdbxCreds("123".getBytes()), inputStream); + } + + @Override + public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { + database.save(credentials, outputStream); + } +} \ No newline at end of file diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java index 73e5bb0a..ef6497bd 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java @@ -75,7 +75,7 @@ public static String fromBoolean(Boolean value) { return value == null ? "False" : (value ? "True" : "False"); } - private static SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + private static SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"); private static Date baseDate; diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java index 86693b4e..a49cfbe4 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java @@ -60,7 +60,22 @@ public enum CompressionFlags { */ @SuppressWarnings("WeakerAccess, unused") public enum ProtectedStreamAlgorithm { - NONE, ARC_FOUR, SALSA_20, CHA_CHA_20 + NONE(0), ARC_FOUR(1), SALSA_20(2), CHA_CHA_20(3); + + private int value; + + ProtectedStreamAlgorithm(int value) { + this.value = value; + } + + public static ProtectedStreamAlgorithm getAlgorithm(int innerRandomStreamId) { + for (ProtectedStreamAlgorithm pse: values()) { + if (pse.value == innerRandomStreamId) { + return pse; + } + } + throw new IllegalArgumentException("Inner Random Stream Id " + innerRandomStreamId + "is not known"); + } } private List allowableVersions = new ArrayList<>(Arrays.asList(3, 4)); @@ -88,14 +103,22 @@ public enum ProtectedStreamAlgorithm { private byte[] streamStartBytes; /* dictionaries in V4 */ - private VariantDictionary kdfparameters; + private VariantDictionary kdfParameters; // TODO implement V4 custom data + @SuppressWarnings({"unused", "FieldCanBeLocal"}) private VariantDictionary customData; - /* not transmitted as part of the header, used in the XML payload, so calculated + /* + * binaries in V4 + * first byte, if set to 1 indicates "protected" remainder is the payload + */ + List binaries = new ArrayList<>(); + + /* V3 not transmitted as part of the header, used in the XML payload, so calculated * on transmission or receipt */ private byte[] headerHash; + /* the bytes that compose the outer header, required for V4 to calculate the HMac */ private byte[] headerBytes; /** @@ -119,7 +142,7 @@ public KdbxHeader(int version) { streamStartBytes = new byte[32]; protectedStreamAlgorithm = ProtectedStreamAlgorithm.SALSA_20; - kdfparameters = Aes.createKdfParameters(); + kdfParameters = Aes.createKdfParameters(); } /** @@ -191,11 +214,11 @@ public StreamEncryptor getInnerStreamEncryptor() { */ public byte[] getTransformedKeyDigest(byte[] digest) { // v3 doesn't have a kdf therefore AES - if (kdfparameters == null) { + if (kdfParameters == null) { return Aes.getTransformedKey(digest, transformSeed, transformRounds); } - KeyDerivationFunction kdf = Encryption.Kdf.getKdf(kdfparameters.mustGet("$UUID").asUuid()); - return kdf.getTransformedKey(digest, kdfparameters); + KeyDerivationFunction kdf = Encryption.Kdf.getKdf(kdfParameters.mustGet("$UUID").asUuid()); + return kdf.getTransformedKey(digest, kdfParameters); } /** @@ -218,14 +241,14 @@ public byte[] getTransformSeed() { if (version < 4) { return transformSeed; } - return kdfparameters.mustGet(Aes.KdfKeys.ParamSeed).asByteArray(); + return kdfParameters.mustGet(Aes.KdfKeys.ParamSeed).asByteArray(); } public long getTransformRounds() { if (version < 4) { return transformRounds; } - return kdfparameters.mustGet(Aes.KdfKeys.ParamRounds).asLong(); + return kdfParameters.mustGet(Aes.KdfKeys.ParamRounds).asLong(); } public UUID getCipherUuid() { @@ -311,7 +334,7 @@ public void setStreamStartBytes(byte[] streamStartBytes) { } public void setInnerRandomStreamId(int innerRandomStreamId) { - this.protectedStreamAlgorithm = ProtectedStreamAlgorithm.values()[innerRandomStreamId]; + this.protectedStreamAlgorithm = ProtectedStreamAlgorithm.getAlgorithm(innerRandomStreamId); } public void setHeaderHash(byte[] headerHash) { @@ -334,22 +357,41 @@ public void setVersion(int version) { this.version = version; } - public void setKdfparameters(VariantDictionary kdfparameters) { - this.kdfparameters = kdfparameters; + /** + * V4 add Key Definition Function Parameters + */ + public void setKdfParameters(VariantDictionary kdfParameters) { + this.kdfParameters = kdfParameters; } + /** + * V4 Add custom data + */ public void setCustomData(VariantDictionary customData) { this.customData = customData; } + /** + * V4 add binary from inner header + */ public void addBinary(byte[] bytes) { - // TODO something about binaries in V4 + binaries.add(bytes); + } + + public List getBinaries() { + return binaries; } + /** + * V4 provide access to the header as bytes for verification + */ public byte[] getHeaderBytes() { return headerBytes; } + /** + * V4 provide access to the header as bytes for verification + */ public void setHeaderBytes(byte[] headerBytes) { byte[] copy = new byte[headerBytes.length]; System.arraycopy(headerBytes, 0, copy, 0, headerBytes.length); diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java index c0a4d364..c910e13a 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java @@ -235,7 +235,7 @@ public static KdbxHeader readOuterHeader(InputStream inputStream, KdbxHeader kdb if (!verifyMagicNumber(ledis)) { throw new IllegalStateException("Magic number did not match"); } - // followed by a file vesion number + // followed by a file version number int fullVersion = ledis.readInt(); kdbxHeader.setVersion(fullVersion >> 16); @@ -270,7 +270,6 @@ public static void verifyOuterHeader(KdbxHeader kdbxHeader, Credentials credenti byte [] hmacKey64 = Encryption.transformHmacKey(hmacKey, Helpers.toBytes(-1L, ByteOrder.LITTLE_ENDIAN)); kdbxHeader.verifyHeaderHmac(hmacKey64, getBytes(32, input)); - } private static void getOuterHeaderFields(KdbxHeader kdbxHeader, MessageDigest digest, DataInput input) throws IOException { @@ -327,7 +326,7 @@ private static void getOuterHeaderFields(KdbxHeader kdbxHeader, MessageDigest di break; case HeaderType.KDF_PARAMETERS: - kdbxHeader.setKdfparameters(makeVariantDictionary(length, input)); + kdbxHeader.setKdfParameters(makeVariantDictionary(length, input)); break; case HeaderType.CUSTOM_DATA: diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java index f9d9b587..ab8248de 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java @@ -70,6 +70,13 @@ public void load(SerializableDatabase serializableDatabase, Credentials credenti if (kdbxHeader.getVersion() == 3 && !Arrays.equals(serializableDatabase.getHeaderHash(), kdbxHeader.getHeaderHash())) { throw new IllegalStateException("Header hash does not match"); } + if (kdbxHeader.getVersion() == 4) { + int count = 0; + for (byte[] binary: kdbxHeader.getBinaries()) { + serializableDatabase.addBinary(count, Arrays.copyOfRange(binary,1, binary.length)); + count++; + } + } decryptedInputStream.close(); } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/SerializableDatabase.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/SerializableDatabase.java index a7ab2bf5..32fa5084 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/SerializableDatabase.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/SerializableDatabase.java @@ -46,4 +46,6 @@ public interface SerializableDatabase { byte[] getHeaderHash(); void setHeaderHash(byte[] hash); + + void addBinary(int index, byte[] payload); } diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java index 3d519cf0..67d0713b 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java @@ -17,6 +17,7 @@ package org.linguafranca.pwdb.kdbx.simple; import org.linguafranca.pwdb.base.AbstractDatabase; +import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.StreamEncryptor; import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxInputTransformer; import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxOutputTransformer; @@ -196,9 +197,34 @@ public static SimpleDatabase load(Credentials credentials, InputStream inputStre throw new IllegalStateException("Header Hash Mismatch"); } + if (kdbxHeader.getVersion() == 4) { + int index = 0; + for (byte[] binary : kdbxHeader.getBinaries()) { + addBinary(result, binary, index); + index++; + } + } + return new SimpleDatabase(result); } + public void addBinary(byte[] bytes, Integer index) { + addBinary(keePassFile, bytes, index); + } + + public static void addBinary(KeePassFile keePassFile, byte[] bytes, Integer index) { + // create a new binary to put in the store + KeePassFile.Binary newBin = new KeePassFile.Binary(); + newBin.setId(index); + newBin.setValue(Helpers.encodeBase64Content(bytes, true)); + newBin.setCompressed(true); + if (keePassFile.getBinaries() == null) { + keePassFile.createBinaries(); + } + keePassFile.getBinaries().add(newBin); + } + + /** * Save as plaintext XML * @@ -251,7 +277,7 @@ public boolean shouldProtect(String s) { } - public List getBinaries() { + public List getBinaries() { return keePassFile.getBinaries(); } diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java index 4446ab2e..93f4f105 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java @@ -145,8 +145,8 @@ public byte[] getBinaryProperty(String s) { return null; } - KeePassFile.Binaries.Binary binary = null; - for (KeePassFile.Binaries.Binary b : database.getBinaries()) { + KeePassFile.Binary binary = null; + for (KeePassFile.Binary b : database.getBinaries()) { if (b.getId().equals(Integer.valueOf(getBinaryContent(bp)))) { binary = b; } @@ -167,19 +167,14 @@ public void setBinaryProperty(String s, byte[] bytes) { // what is the next free index in the binary store? Integer max = -1; - for (KeePassFile.Binaries.Binary binary: database.getBinaries()){ + for (KeePassFile.Binary binary: database.getBinaries()){ if (binary.getId() > max) { max = binary.getId(); } } max++; - // create a new binary to put in the store - KeePassFile.Binaries.Binary newBin = new KeePassFile.Binaries.Binary(); - newBin.setId(max); - newBin.setValue(Helpers.encodeBase64Content(bytes, true)); - newBin.setCompressed(true); - database.getBinaries().add(newBin); + database.addBinary(bytes, max); // make a reference to it from the entry BinaryProperty binaryProperty = new BinaryProperty(); diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java index 8eb46380..b33f582c 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java @@ -42,10 +42,14 @@ public class KeePassFile { @Element(name = "Root") public Root root; - public List getBinaries() { + public List getBinaries() { return meta.binaries; } + public void createBinaries() { + meta.binaries = new ArrayList<>(); + } + public static class Root { @Element(name = "Group") public SimpleGroup group; @@ -120,7 +124,7 @@ public static class Meta { @Element(name = "HistoryMaxSize") protected int historyMaxSize; @ElementList(name = "Binaries", required = false) - protected List binaries; + protected List binaries; @Element(name = "CustomData", required = false) protected KeePassFile.CustomData customData; @@ -162,48 +166,47 @@ public Boolean shouldProtect(String name) { } public static class Binaries { - @ElementList(name = "Binary") - protected List binary; - - @org.simpleframework.xml.Root(name = "Binary") - public static class Binary implements org.simpleframework.xml.util.Entry { - @Text - protected String value; - - @Attribute(name = "ID") - protected Integer id; - @Attribute(name = "Compressed") - @Convert(KeePassBooleanConverter.class) - protected Boolean compressed; - - @Override - public String getName() { - return String.valueOf(id); - } - public String getValue() { - return value; - } + } - public Boolean getCompressed() { - return compressed; - } + @org.simpleframework.xml.Root(name = "Binary") + public static class Binary implements org.simpleframework.xml.util.Entry { + @Text + protected String value; - public Integer getId() { - return id; - } + @Attribute(name = "ID") + protected Integer id; + @Attribute(name = "Compressed") + @Convert(KeePassBooleanConverter.class) + protected Boolean compressed; - public void setId(Integer Id) { - this.id = Id; - } + @Override + public String getName() { + return String.valueOf(id); + } - public void setValue(String value) { - this.value = value; - } + public String getValue() { + return value; + } - public void setCompressed(boolean compressed) { - this.compressed = compressed; - } + public Boolean getCompressed() { + return compressed; + } + + public Integer getId() { + return id; + } + + public void setId(Integer Id) { + this.id = Id; + } + + public void setValue(String value) { + this.value = value; + } + + public void setCompressed(boolean compressed) { + this.compressed = compressed; } } diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntryTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV3Test.java similarity index 92% rename from simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntryTest.java rename to simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV3Test.java index b8cd5f00..71ca5944 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntryTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV3Test.java @@ -28,9 +28,9 @@ /** * @author jo */ -public class SimpleEntryTest extends BinaryPropertyChecks { +public class SimpleBinaryPropertyV3Test extends BinaryPropertyChecks { - public SimpleEntryTest() { + public SimpleBinaryPropertyV3Test() { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment.kdbx"); try { database = SimpleDatabase.load(new KdbxCreds("123".getBytes()),inputStream); diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java new file mode 100644 index 00000000..c94ee8a4 --- /dev/null +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015 Jo Rabin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx.simple; + +import org.junit.Ignore; +import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.checks.BinaryPropertyChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * @author jo + */ +@Ignore +public class SimpleBinaryPropertyV4Test extends BinaryPropertyChecks { + + public SimpleBinaryPropertyV4Test() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment-ChaCha20-Argon2.kdbx"); + database = SimpleDatabase.load(new KdbxCreds("123".getBytes()), inputStream); + } + + @Override + public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { + database.save(credentials, outputStream); + } +} \ No newline at end of file diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java index da17cee9..cdb40f24 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java @@ -20,6 +20,8 @@ import org.junit.Test; import org.linguafranca.pwdb.*; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.List; import static org.junit.Assert.assertEquals; @@ -30,11 +32,12 @@ public abstract class DatabaseLoaderChecks , G extends Group, E extends Entry, I extends Icon>{ protected Database database; + private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssX"); /** * a test123 file for each format. Should contain the same thing. This is a basic sanity check. */ @Test - public void test123File() { + public void test123File() throws ParseException { // visit all groups and entries and list them to console database.visit(new Visitor.Print()); @@ -79,5 +82,7 @@ public boolean matches(Entry entry) { Assert.assertEquals(1, entries.size()); assertEquals("pass", entries.get(0).getPassword()); + + Assert.assertEquals(sdf.parse("2015-10-24T17:20:41Z"),entries.get(0).getCreationTime()); } } diff --git a/test/src/main/resources/Attachment-ChaCha20-Argon2.kdbx b/test/src/main/resources/Attachment-ChaCha20-Argon2.kdbx new file mode 100755 index 0000000000000000000000000000000000000000..59b73e50384873b5a535397de194d41c067b326d GIT binary patch literal 9842 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 M=_4ADn8aOSG(R%AG5`Po literal 0 HcmV?d00001 diff --git a/test/src/main/resources/test123-AES-Argon2.kdbx b/test/src/main/resources/test123-AES-Argon2.kdbx new file mode 100755 index 0000000000000000000000000000000000000000..1f95bc4391a8f87b672fd3947d2ecf696b012537 GIT binary patch literal 3509 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 literal 0 HcmV?d00001 diff --git a/test/src/main/resources/test123-ChaCha20-Argon2.kdbx b/test/src/main/resources/test123-ChaCha20-Argon2.kdbx new file mode 100755 index 0000000000000000000000000000000000000000..7722d6f2e8ea456f7601c1d5d496625b116fa17c GIT binary patch literal 3514 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 Date: Wed, 14 Feb 2018 14:57:34 +0000 Subject: [PATCH 075/228] Fixing KDB failed test problem on travis --- .../org/linguafranca/pwdb/kdb/KdbSerializer.java | 14 +++++++++----- .../pwdb/kdb/KdbDatabaseLoaderTest.java | 1 + .../pwdb/checks/DatabaseLoaderChecks.java | 12 ++++++++++-- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java index 62b0891d..d4256f87 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java @@ -27,10 +27,7 @@ import java.nio.ByteBuffer; import java.security.DigestInputStream; import java.security.MessageDigest; -import java.util.Arrays; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.UUID; +import java.util.*; /** * This class provides support for reading a KDB stream and constructing an in memory database. @@ -362,7 +359,14 @@ public static Date unpackDate(byte[] buffer) { int year = (int) longValue & 0xFFF; // just to work around the deprecation on the similar Date constructor - return new GregorianCalendar(year, month - 1, day, hour, minute, second).getTime(); + GregorianCalendar cal = new GregorianCalendar(); + // I think the time is stored in local time but anyway, let's say it's UTC for the sake of argument + cal.setTimeZone(TimeZone.getTimeZone("UTC")); + //noinspection MagicConstant + cal.set(year, month - 1, day, hour, minute, second); + // otherwise we seems to end up with arbitrary millis + cal.set(GregorianCalendar.MILLISECOND, 0); + return cal.getTime(); } /**** diff --git a/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbDatabaseLoaderTest.java b/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbDatabaseLoaderTest.java index 5b5ecc15..d375e45f 100644 --- a/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbDatabaseLoaderTest.java +++ b/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbDatabaseLoaderTest.java @@ -34,5 +34,6 @@ public KdbDatabaseLoaderTest() throws IOException { Credentials credentials = new KdbCredentials.Password("123".getBytes()); // open database. super.database = KdbDatabase.load(credentials, inputStream); + super.skipDateCheck = true; } } diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java index cdb40f24..eb726659 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java @@ -22,6 +22,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Date; import java.util.List; import static org.junit.Assert.assertEquals; @@ -31,7 +32,7 @@ */ public abstract class DatabaseLoaderChecks , G extends Group, E extends Entry, I extends Icon>{ protected Database database; - + protected boolean skipDateCheck = false; private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssX"); /** * a test123 file for each format. Should contain the same thing. This is a basic sanity check. @@ -83,6 +84,13 @@ public boolean matches(Entry entry) { Assert.assertEquals(1, entries.size()); assertEquals("pass", entries.get(0).getPassword()); - Assert.assertEquals(sdf.parse("2015-10-24T17:20:41Z"),entries.get(0).getCreationTime()); + // kdb files don't have a time zone so can't make head or tail of the date - test file seems to have a local time in it + if (skipDateCheck) { + return; + } + + Date c = entries.get(0).getCreationTime(); + Date expected = sdf.parse("2015-10-24T17:20:41Z"); + Assert.assertEquals(expected, c); } } From cbc984e702972046d9671f0406aaa28f9b0d101d Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 20 Feb 2018 17:34:30 +0000 Subject: [PATCH 076/228] centralize creation of automatic module id for Java 9 --- database/pom.xml | 16 ---------------- dom/pom.xml | 16 ---------------- jaxb/pom.xml | 11 ----------- kdb/pom.xml | 17 ----------------- kdbx/pom.xml | 17 ----------------- pom.xml | 7 +++++++ simple/pom.xml | 17 ----------------- 7 files changed, 7 insertions(+), 94 deletions(-) diff --git a/database/pom.xml b/database/pom.xml index 64a4e81b..959be4c8 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -37,20 +37,4 @@ runtime - - - - - org.apache.maven.plugins - maven-jar-plugin - - - - org.linguafranca.pwdb.database - - - - - - \ No newline at end of file diff --git a/dom/pom.xml b/dom/pom.xml index 5e9d4864..9b425739 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -29,20 +29,4 @@ test - - - - - org.apache.maven.plugins - maven-jar-plugin - - - - org.linguafranca.pwdb.kdbx.dom - - - - - - \ No newline at end of file diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 872d36d6..cbf74713 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -59,17 +59,6 @@ src/generated/java - - org.apache.maven.plugins - maven-jar-plugin - - - - org.linguafranca.pwdb.kdbx.jaxb - - - - diff --git a/kdb/pom.xml b/kdb/pom.xml index 921eb6d4..d0735d6e 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -29,21 +29,4 @@ test - - - - - org.apache.maven.plugins - maven-jar-plugin - - - - org.linguafranca.pwdb.kdb - - - - - - - \ No newline at end of file diff --git a/kdbx/pom.xml b/kdbx/pom.xml index f727bc85..7c289a88 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -58,21 +58,4 @@ test - - - - - org.apache.maven.plugins - maven-jar-plugin - - - - org.linguafranca.pwdb.kdbx - - - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index cd81c089..f3fab033 100644 --- a/pom.xml +++ b/pom.xml @@ -96,6 +96,13 @@ org.apache.maven.plugins maven-jar-plugin ${maven-jar-plugin.version} + + + + ${project.groupId}.${project.artifactId} + + + diff --git a/simple/pom.xml b/simple/pom.xml index 9400097a..f34c6bab 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -61,21 +61,4 @@ 1.0.0 - - - - - org.apache.maven.plugins - maven-jar-plugin - - - - org.linguafranca.pwdb.kdbx.simple - - - - - - - \ No newline at end of file From a034097434dc0dcc3f87598f3a59b4f09a8b1e3d Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 20 Feb 2018 17:47:01 +0000 Subject: [PATCH 077/228] get rid of extraneous temp.xml created on build --- .../linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java index 1a8adf3d..ba059e7c 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java @@ -10,6 +10,9 @@ import org.linguafranca.pwdb.kdbx.KdbxSerializer; import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.attribute.FileAttribute; import java.util.Scanner; /** @@ -58,7 +61,7 @@ public void getDecryptedArgonInputStream() throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); InputStream is = KdbxSerializer.createUnencryptedInputStream(new KdbxCreds("123".getBytes()), new KdbxHeader(), inputStream); - PrintWriter pw = new PrintWriter(new FileOutputStream("temp.xml")); + PrintWriter pw = new PrintWriter(new FileOutputStream(Files.createTempFile("temp", "xml").toFile())); Scanner s = new Scanner(is); while (s.hasNext()) { pw.println(s.nextLine()); From 3b5e359dc64ed4545d4107d0796d242545d00a09 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 20 Feb 2018 19:08:25 +0000 Subject: [PATCH 078/228] Improve KDBX tests --- .../pwdb/kdbx/stream/KdbxSerializerTest.java | 72 ++-- kdbx/src/test/resources/AES-AES.xml | 313 ++++++++++++++++++ kdbx/src/test/resources/AES-Argon2.xml | 266 +++++++++++++++ kdbx/src/test/resources/ChaCha-AES.xml | 313 ++++++++++++++++++ 4 files changed, 919 insertions(+), 45 deletions(-) create mode 100644 kdbx/src/test/resources/AES-AES.xml create mode 100644 kdbx/src/test/resources/AES-Argon2.xml create mode 100644 kdbx/src/test/resources/ChaCha-AES.xml diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java index ba059e7c..3e66bee4 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java @@ -1,33 +1,27 @@ package org.linguafranca.pwdb.kdbx.stream; +import com.google.common.io.CharStreams; import com.google.common.io.LittleEndianDataInputStream; import org.junit.Ignore; import org.junit.Test; import org.linguafranca.pwdb.hashedblock.HmacBlockInputStream; -import org.linguafranca.pwdb.kdbx.KdbxCredentials; import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.kdbx.KdbxHeader; import org.linguafranca.pwdb.kdbx.KdbxSerializer; -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.nio.file.attribute.FileAttribute; -import java.util.Scanner; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; + +import static org.junit.Assert.assertEquals; /** * @author jo */ public class KdbxSerializerTest { - @Test - public void loadArgonheader() throws IOException { - InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); - KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); - System.out.println("Version " + header.getVersion()); - } - - @Test + @Test @Ignore // can be used for detailed HMAC debugging public void getHmacStream() throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); @@ -46,51 +40,39 @@ public void getHmacStream() throws IOException { public void loadAesheader() throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-AES.kdbx"); KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); - System.out.println("Version " + header.getVersion()); + assertEquals(3, header.getVersion()); } @Test - public void loadChaChaAesHeader() throws IOException { - InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha-AES.kdbx"); + public void loadArgonheader() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); - System.out.println("Version " + header.getVersion()); + assertEquals(4, header.getVersion()); + } + + + @Test + public void loadChaChaAesHeader() throws IOException { + String expected = CharStreams.toString(new InputStreamReader(KdbxSerializerTest.class.getClassLoader().getResourceAsStream("ChaCha-AES.xml"))); + check("ChaCha-AES.xml","V4-ChaCha-AES.kdbx"); } @Test public void getDecryptedArgonInputStream() throws IOException { - InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); - InputStream is = KdbxSerializer.createUnencryptedInputStream(new KdbxCreds("123".getBytes()), new KdbxHeader(), inputStream); - - PrintWriter pw = new PrintWriter(new FileOutputStream(Files.createTempFile("temp", "xml").toFile())); - Scanner s = new Scanner(is); - while (s.hasNext()) { - pw.println(s.nextLine()); - } - s.close(); - pw.close(); + String expected = CharStreams.toString(new InputStreamReader(KdbxSerializerTest.class.getClassLoader().getResourceAsStream("AES-Argon2.xml"))); + check("AES-Argon2.xml","V4-AES-Argon2.kdbx"); } @Test public void getDecryptedAesInputStream() throws IOException { - InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-AES.kdbx"); - InputStream is = KdbxSerializer.createUnencryptedInputStream(new KdbxCreds("123".getBytes()), new KdbxHeader(), inputStream); - Scanner s = new Scanner(is); - while (s.hasNext()) { - System.out.println(s.nextLine()); - } - s.close(); + check("AES-AES.xml", "V4-AES-AES.kdbx"); } - @Test - public void getDecryptedCHaChaInputStream() throws IOException { - InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha-AES.kdbx"); + private void check(String compare, String file) throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(file); InputStream is = KdbxSerializer.createUnencryptedInputStream(new KdbxCreds("123".getBytes()), new KdbxHeader(), inputStream); - Scanner s = new Scanner(is); - while (s.hasNext()) { - System.out.println(s.nextLine()); - } - s.close(); - + String actual = CharStreams.toString(new InputStreamReader(is, Charset.forName("UTF-8"))); + String expected = CharStreams.toString(new InputStreamReader(KdbxSerializerTest.class.getClassLoader().getResourceAsStream(compare))); + assertEquals(expected, actual); } - } \ No newline at end of file diff --git a/kdbx/src/test/resources/AES-AES.xml b/kdbx/src/test/resources/AES-AES.xml new file mode 100644 index 00000000..f77f9e1b --- /dev/null +++ b/kdbx/src/test/resources/AES-AES.xml @@ -0,0 +1,313 @@ + + + + KeePass + V2s6F1AQsmPRea/yyukPa+C2kFstdTA+zcJBgeIgKDg= + New V4 Database + 2018-01-25T13:28:56Z + This is a new database + 2018-01-25T13:28:56Z + + 2018-01-25T13:24:05Z + 365 + + 2018-01-25T13:24:05Z + -1 + -1 + + False + False + True + False + False + + True + AAAAAAAAAAAAAAAAAAAAAA== + 2018-01-25T13:24:05Z + AAAAAAAAAAAAAAAAAAAAAA== + 2018-01-25T13:24:05Z + 10 + 6291456 + 3mzTUVoVAkO7DFaZ6Soxrw== + 3mzTUVoVAkO7DFaZ6Soxrw== + + + + + + 3mzTUVoVAkO7DFaZ6Soxrw== + NewV4Database + + 49 + + 2018-01-25T13:24:05Z + 2018-01-25T13:24:05Z + 2018-01-25T13:29:25Z + 2018-01-25T13:23:08Z + False + 4 + 2018-01-25T13:24:05Z + + True + + null + null + t53t29Y1A0abjVS1+xTo5g== + + t53t29Y1A0abjVS1+xTo5g== + 0 + + + + + + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:23:08Z + False + 0 + 2018-01-25T13:28:56Z + + + Notes + Notes + + + Password + U6biOGfHsEc= + + + Title + Sample Entry + + + URL + https://keepass.info/ + + + UserName + User Name + + + True + 0 + + Target Window + {USERNAME}{TAB}{PASSWORD}{TAB}{ENTER} + + + + + + YeputjepZky3vXID4/HHrA== + 0 + + + + + + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:23:08Z + False + 0 + 2018-01-25T13:28:56Z + + + Password + jywfUBg= + + + Title + Sample Entry #2 + + + URL + https://keepass.info/help/kb/testform.html + + + UserName + Michael321 + + + True + 0 + + *Test Form - KeePass* + + + + + + + TyxuU/7td0iU8+b0iHRh3w== + 0 + + + + + + 2018-01-26T13:20:58Z + 2018-01-25T13:28:56Z + 2018-01-26T13:22:20Z + 2018-01-25T13:23:08Z + False + 1 + 2018-01-26T13:20:58Z + + + Notes + + + + Password + BRBPZpg= + + + Title + Sample Entry #2 - Copy + + + URL + https://keepass.info/help/kb/testform.html + + + UserName + Michael321 + + + True + 0 + + *Test Form - KeePass* + + + + + + + 3BxuF+oUekuMpLe67xmTLQ== + General + + 48 + + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:29:24Z + 2018-01-25T13:23:08Z + False + 2 + 2018-01-25T13:28:56Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + vQsKl6dJiEiNvbTkjDVHcw== + Windows + + 38 + + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:23:08Z + False + 0 + 2018-01-25T13:28:56Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + 6bNmbKf/QUKN7I3GlNqqQw== + Network + + 3 + + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:23:08Z + False + 0 + 2018-01-25T13:28:56Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + Qz8rqQvYZUummUrakV6BFQ== + Internet + + 1 + + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:23:08Z + False + 0 + 2018-01-25T13:28:56Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + WMP+6xrurE+Jr6kFDwJtxQ== + eMail + + 19 + + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:23:08Z + False + 0 + 2018-01-25T13:28:56Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + mSY4yshaIU64BNreFBm+1w== + Homebanking + + 37 + + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:29:23Z + 2018-01-25T13:23:08Z + False + 1 + 2018-01-25T13:28:56Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + + + \ No newline at end of file diff --git a/kdbx/src/test/resources/AES-Argon2.xml b/kdbx/src/test/resources/AES-Argon2.xml new file mode 100644 index 00000000..c488f75e --- /dev/null +++ b/kdbx/src/test/resources/AES-Argon2.xml @@ -0,0 +1,266 @@ + + + + KeePass + mNL70Q4AAAA= + New V4 Database + mNL70Q4AAAA= + This is a new database + mNL70Q4AAAA= + + ddH70Q4AAAA= + 365 + + ddH70Q4AAAA= + -1 + -1 + + False + False + True + False + False + + True + AAAAAAAAAAAAAAAAAAAAAA== + ddH70Q4AAAA= + AAAAAAAAAAAAAAAAAAAAAA== + ddH70Q4AAAA= + 10 + 6291456 + 3mzTUVoVAkO7DFaZ6Soxrw== + 3mzTUVoVAkO7DFaZ6Soxrw== + + + + + 3mzTUVoVAkO7DFaZ6Soxrw== + NewV4Database + + 49 + + ddH70Q4AAAA= + ddH70Q4AAAA= + tdL70Q4AAAA= + PNH70Q4AAAA= + False + 4 + ddH70Q4AAAA= + + True + + null + null + t53t29Y1A0abjVS1+xTo5g== + + t53t29Y1A0abjVS1+xTo5g== + 0 + + + + + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + + Notes + Notes + + + Password + 6f9J8T82DJ0= + + + Title + Sample Entry + + + URL + https://keepass.info/ + + + UserName + User Name + + + True + 0 + + Target Window + {USERNAME}{TAB}{PASSWORD}{TAB}{ENTER} + + + + + + YeputjepZky3vXID4/HHrA== + 0 + + + + + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + + Password + heKymtY= + + + Title + Sample Entry #2 + + + URL + https://keepass.info/help/kb/testform.html + + + UserName + Michael321 + + + True + 0 + + *Test Form - KeePass* + + + + + + + 3BxuF+oUekuMpLe67xmTLQ== + General + + 48 + + mNL70Q4AAAA= + mNL70Q4AAAA= + tNL70Q4AAAA= + PNH70Q4AAAA= + False + 2 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + vQsKl6dJiEiNvbTkjDVHcw== + Windows + + 38 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + 6bNmbKf/QUKN7I3GlNqqQw== + Network + + 3 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + Qz8rqQvYZUummUrakV6BFQ== + Internet + + 1 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + WMP+6xrurE+Jr6kFDwJtxQ== + eMail + + 19 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + mSY4yshaIU64BNreFBm+1w== + Homebanking + + 37 + + mNL70Q4AAAA= + mNL70Q4AAAA= + s9L70Q4AAAA= + PNH70Q4AAAA= + False + 1 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + + + \ No newline at end of file diff --git a/kdbx/src/test/resources/ChaCha-AES.xml b/kdbx/src/test/resources/ChaCha-AES.xml new file mode 100644 index 00000000..f0a7968f --- /dev/null +++ b/kdbx/src/test/resources/ChaCha-AES.xml @@ -0,0 +1,313 @@ + + + + KeePass + qiv6mbGxmbZVNGs/0A7xbFUe9D8SxOcP+7U71ju4gAU= + New V4 Database + 2018-01-25T13:28:56Z + This is a new database + 2018-01-25T13:28:56Z + + 2018-01-25T13:24:05Z + 365 + + 2018-01-25T13:24:05Z + -1 + -1 + + False + False + True + False + False + + True + AAAAAAAAAAAAAAAAAAAAAA== + 2018-01-25T13:24:05Z + AAAAAAAAAAAAAAAAAAAAAA== + 2018-01-25T13:24:05Z + 10 + 6291456 + 3mzTUVoVAkO7DFaZ6Soxrw== + 3mzTUVoVAkO7DFaZ6Soxrw== + + + + + + 3mzTUVoVAkO7DFaZ6Soxrw== + NewV4Database + + 49 + + 2018-01-25T13:24:05Z + 2018-01-25T13:24:05Z + 2018-01-25T13:29:25Z + 2018-01-25T13:23:08Z + False + 4 + 2018-01-25T13:24:05Z + + True + + null + null + t53t29Y1A0abjVS1+xTo5g== + + t53t29Y1A0abjVS1+xTo5g== + 0 + + + + + + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:23:08Z + False + 0 + 2018-01-25T13:28:56Z + + + Notes + Notes + + + Password + hkDT7ZRetpo= + + + Title + Sample Entry + + + URL + https://keepass.info/ + + + UserName + User Name + + + True + 0 + + Target Window + {USERNAME}{TAB}{PASSWORD}{TAB}{ENTER} + + + + + + YeputjepZky3vXID4/HHrA== + 0 + + + + + + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:23:08Z + False + 0 + 2018-01-25T13:28:56Z + + + Password + Ep/7NYY= + + + Title + Sample Entry #2 + + + URL + https://keepass.info/help/kb/testform.html + + + UserName + Michael321 + + + True + 0 + + *Test Form - KeePass* + + + + + + + TyxuU/7td0iU8+b0iHRh3w== + 0 + + + + + + 2018-01-26T13:20:58Z + 2018-01-25T13:28:56Z + 2018-01-26T13:22:20Z + 2018-01-25T13:23:08Z + False + 1 + 2018-01-26T13:20:58Z + + + Notes + + + + Password + /+19knE= + + + Title + Sample Entry #2 - Copy + + + URL + https://keepass.info/help/kb/testform.html + + + UserName + Michael321 + + + True + 0 + + *Test Form - KeePass* + + + + + + + 3BxuF+oUekuMpLe67xmTLQ== + General + + 48 + + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:29:24Z + 2018-01-25T13:23:08Z + False + 2 + 2018-01-25T13:28:56Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + vQsKl6dJiEiNvbTkjDVHcw== + Windows + + 38 + + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:23:08Z + False + 0 + 2018-01-25T13:28:56Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + 6bNmbKf/QUKN7I3GlNqqQw== + Network + + 3 + + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:23:08Z + False + 0 + 2018-01-25T13:28:56Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + Qz8rqQvYZUummUrakV6BFQ== + Internet + + 1 + + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:23:08Z + False + 0 + 2018-01-25T13:28:56Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + WMP+6xrurE+Jr6kFDwJtxQ== + eMail + + 19 + + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:23:08Z + False + 0 + 2018-01-25T13:28:56Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + mSY4yshaIU64BNreFBm+1w== + Homebanking + + 37 + + 2018-01-25T13:28:56Z + 2018-01-25T13:28:56Z + 2018-01-25T13:29:23Z + 2018-01-25T13:23:08Z + False + 1 + 2018-01-25T13:28:56Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + + + \ No newline at end of file From 0a80994862bb594e8afc76b6e4a5ec7d6e57eaf4 Mon Sep 17 00:00:00 2001 From: dgrahl Date: Sat, 15 May 2021 10:13:20 +0200 Subject: [PATCH 079/228] #29 change MessageDigest in HashedBlock...Stream from static to instance --- .../hashedblock/HashedBlockInputStream.java | 23 ++++++++--------- .../hashedblock/HashedBlockOutputStream.java | 25 ++++++++++--------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockInputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockInputStream.java index 80dabb74..91a20d49 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockInputStream.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockInputStream.java @@ -16,8 +16,6 @@ package org.linguafranca.pwdb.hashedblock; -import org.jetbrains.annotations.NotNull; - import java.io.ByteArrayInputStream; import java.io.EOFException; import java.io.IOException; @@ -26,6 +24,8 @@ import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import org.jetbrains.annotations.NotNull; + /** * Takes an underlying input stream formatted as Hashed Blocks * and provides the content of the blocks as an input stream @@ -49,17 +49,10 @@ */ public class HashedBlockInputStream extends InputStream { - private static MessageDigest md5; - static { - try { - md5 = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } - } private static final int HASH_SIZE = 32; private static final byte[] ZERO_HASH = new byte[HASH_SIZE]; + private final MessageDigest sha256; private long expectedSequenceNumber = 0; private boolean littleEndian = false; private boolean done = false; @@ -82,6 +75,12 @@ public HashedBlockInputStream(InputStream inputStream) { public HashedBlockInputStream(InputStream inputStream, boolean littleEndian) { this.inputStream = inputStream; this.littleEndian = littleEndian; + try { + sha256 = MessageDigest.getInstance( "SHA-256" ); + } + catch( NoSuchAlgorithmException e ) { + throw new IllegalStateException( e ); + } } @Override @@ -165,8 +164,8 @@ protected void load() throws IOException { readFully(readBuffer); // check the hash - md5.update(readBuffer); - if (!Arrays.equals(md5.digest(), hash)) { + sha256.update(readBuffer); + if (!Arrays.equals(sha256.digest(), hash)) { throw new IllegalStateException("MD5 check failed while reading HashBlock"); } blockInputStream = new ByteArrayInputStream(readBuffer); diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockOutputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockOutputStream.java index 0af0e45e..a7a94f77 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockOutputStream.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockOutputStream.java @@ -16,8 +16,6 @@ package org.linguafranca.pwdb.hashedblock; -import org.jetbrains.annotations.NotNull; - import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.IOException; @@ -25,6 +23,8 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import org.jetbrains.annotations.NotNull; + /** * Takes a stream of data and formats as Hashed Blocks to the underlying output stream. * @@ -47,18 +47,11 @@ */ public class HashedBlockOutputStream extends OutputStream { - private static MessageDigest md5; - static { - try { - md5 = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } - } private static final int BLOCK_SIZE = 8 * 1024; private static final int HASH_SIZE = 32; private static final byte[] ZERO_HASH = new byte[HASH_SIZE]; + private final MessageDigest sha256; private int nextSequenceNumber = 0; private boolean littleEndian = false; private OutputStream outputStream; @@ -81,6 +74,14 @@ public HashedBlockOutputStream(OutputStream outputStream) { public HashedBlockOutputStream(OutputStream outputStream, boolean littleEndian) { this.outputStream = outputStream; this.littleEndian = littleEndian; + try + { + sha256 = MessageDigest.getInstance( "SHA-256" ); + } + catch( NoSuchAlgorithmException e ) + { + throw new IllegalStateException( e ); + } } @Override @@ -152,8 +153,8 @@ protected void save() throws IOException { // calculate the hash of the buffer byte[] buffer = blockOutputStream.toByteArray(); - md5.update(buffer); - outputStream.write(md5.digest()); + sha256.update(buffer); + outputStream.write(sha256.digest()); // write the buffer's length writeInt(buffer.length); From 4a54b27f8af29d15016d5195a25518a7ab9c3de6 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Wed, 3 May 2023 10:33:10 +0100 Subject: [PATCH 080/228] Remove Jargon2 and spongyCastle, replace with bouncyCastle. Update dependency versions. --- .gitignore | 1 + CHANGELOG.md | 11 ++++- database/pom.xml | 16 ++------ .../org/linguafranca/pwdb/security/Aes.java | 16 ++++---- .../linguafranca/pwdb/security/Argon2.java | 32 ++++++++------- .../linguafranca/pwdb/security/ChaCha.java | 12 +++--- http/pom.xml | 2 +- .../linguafranca/pwdb/keepasshttp/Crypto.java | 12 +++--- .../pwdb/keepasshttp/DatabaseAdaptor.java | 2 +- .../pwdb/keepasshttp/TestRequestResponse.java | 2 +- .../linguafranca/pwdb/kdb/KdbCredentials.java | 2 +- .../hashedblock/HashedBlockInputStream.java | 21 ++++++---- .../hashedblock/HashedBlockOutputStream.java | 25 ++++++----- .../linguafranca/pwdb/kdbx/KdbxHeader.java | 17 +++++--- .../pwdb/kdbx/StreamEncryptor.java | 12 +++--- pom.xml | 41 +++++++++---------- simple/pom.xml | 8 ++-- 17 files changed, 123 insertions(+), 109 deletions(-) diff --git a/.gitignore b/.gitignore index c5d45fa4..0e9e51f2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ gradlew.bat .gradle gradle *.versionsBackup +*/target diff --git a/CHANGELOG.md b/CHANGELOG.md index c04ea078..85df56be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,19 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.co ##[2.2-Snapshot] +### Changed + +- Minimum version supported is Java 8 + ### Added -- Preliminary readonly support for V4 files +- Readonly support for V4 files - updated documentation of various sorts +- kdbx multithreaded fix +- update dependencies + - spongy castle replaced by bouncy castle + - simplexml replaced by simple-xml-safe + - update versions throughout ## [2.1.4] 2018-02-03 diff --git a/database/pom.xml b/database/pom.xml index 959be4c8..f632e8ca 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -22,19 +22,9 @@ guava - com.madgag.spongycastle - core - - - com.kosprov.jargon2 - jargon2-api - 1.0.1 - - - com.kosprov.jargon2 - jargon2-native-ri-backend - 1.1.0 - runtime + org.bouncycastle + bcpkix-jdk15on + \ No newline at end of file diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java index ba61191a..ba6a867f 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java @@ -1,12 +1,12 @@ package org.linguafranca.pwdb.security; -import org.spongycastle.crypto.engines.AESEngine; -import org.spongycastle.crypto.io.CipherInputStream; -import org.spongycastle.crypto.io.CipherOutputStream; -import org.spongycastle.crypto.modes.CBCBlockCipher; -import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; -import org.spongycastle.crypto.params.KeyParameter; -import org.spongycastle.crypto.params.ParametersWithIV; +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.io.CipherInputStream; +import org.bouncycastle.crypto.io.CipherOutputStream; +import org.bouncycastle.crypto.modes.CBCBlockCipher; +import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; import java.io.InputStream; import java.io.OutputStream; @@ -95,7 +95,7 @@ public byte[] getTransformedKey(byte[] key, VariantDictionary transformParams) { /** * Simplified version for KDBX V3 - * @param key they composite key + * @param key the composite key * @param transformSeed the seed * @param transformRounds number of rounds * @return a transformed key diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java b/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java index 07166d61..25b67f81 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java @@ -1,10 +1,13 @@ package org.linguafranca.pwdb.security; +import java.nio.charset.StandardCharsets; import java.util.UUID; -import static com.kosprov.jargon2.api.Jargon2.*; import static org.linguafranca.pwdb.security.Argon2.VariantDictionaryKeys.*; +import org.bouncycastle.crypto.generators.Argon2BytesGenerator; +import org.bouncycastle.crypto.params.Argon2Parameters; + /** * KDBX V4 files may use Argon2 for key derivation. @@ -53,24 +56,23 @@ public UUID getKdfUuid() { @Override public byte[] getTransformedKey(byte[] digest, VariantDictionary argonParameterKeys) { - byte bVersion = argonParameterKeys.mustGet(paramVersion).asByteArray()[0]; - Version version = bVersion == 0x13 ? Version.V13 : Version.V10; + int version = argonParameterKeys.mustGet(paramVersion).asInteger(); byte[] salt = argonParameterKeys.mustGet(paramSalt).asByteArray(); int parallelism = argonParameterKeys.mustGet(paramParallelism).asInteger(); int memoryCost = (int) argonParameterKeys.mustGet(paramMemory).asLong(); int timeCost = (int) argonParameterKeys.mustGet(paramIterations).asLong(); - // Configure the hasher - Hasher hasher = jargon2Hasher() - .type(Type.ARGON2d) - .version(version) - .salt(salt) - .parallelism(parallelism) - .memoryCost(memoryCost / 1024) // block size 1024 - .timeCost(timeCost) - .hashLength(32); - - // do the hash - return hasher.password(digest).rawHash(); + Argon2Parameters.Builder builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_d) + .withVersion(version) + .withIterations(timeCost) + .withMemoryAsKB(memoryCost/1024) + .withParallelism(parallelism) + .withSalt(salt); + + Argon2BytesGenerator gen = new Argon2BytesGenerator(); + gen.init(builder.build()); + byte[] result = new byte[32]; + gen.generateBytes(digest, result, 0, result.length); + return result; } } diff --git a/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java b/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java index 558e1900..c9d5d710 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java @@ -1,11 +1,11 @@ package org.linguafranca.pwdb.security; -import org.spongycastle.crypto.StreamCipher; -import org.spongycastle.crypto.engines.ChaCha7539Engine; -import org.spongycastle.crypto.io.CipherInputStream; -import org.spongycastle.crypto.io.CipherOutputStream; -import org.spongycastle.crypto.params.KeyParameter; -import org.spongycastle.crypto.params.ParametersWithIV; +import org.bouncycastle.crypto.StreamCipher; +import org.bouncycastle.crypto.engines.ChaCha7539Engine; +import org.bouncycastle.crypto.io.CipherInputStream; +import org.bouncycastle.crypto.io.CipherOutputStream; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; import java.io.InputStream; import java.io.OutputStream; diff --git a/http/pom.xml b/http/pom.xml index 7757678e..3c701e37 100644 --- a/http/pom.xml +++ b/http/pom.xml @@ -19,7 +19,7 @@ org.eclipse.jetty jetty-server - 9.2.19.v20160908 + 9.4.51.v20230217 org.slf4j diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java index ac6ad90d..284b3edc 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java @@ -1,12 +1,12 @@ package org.linguafranca.pwdb.keepasshttp; import org.linguafranca.pwdb.kdbx.Helpers; -import org.spongycastle.crypto.InvalidCipherTextException; -import org.spongycastle.crypto.engines.AESFastEngine; -import org.spongycastle.crypto.modes.CBCBlockCipher; -import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; -import org.spongycastle.crypto.params.KeyParameter; -import org.spongycastle.crypto.params.ParametersWithIV; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.engines.AESFastEngine; +import org.bouncycastle.crypto.modes.CBCBlockCipher; +import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; import java.security.SecureRandom; diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java index 7ba1fb6f..397c0e1c 100644 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java +++ b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java @@ -5,7 +5,7 @@ import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; -import org.spongycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.crypto.digests.SHA1Digest; import java.io.*; diff --git a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java index 2e2afb20..30775a86 100644 --- a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java +++ b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java @@ -9,7 +9,7 @@ import org.linguafranca.pwdb.keepasshttp.Crypto; import org.linguafranca.pwdb.keepasshttp.Processor; import org.linguafranca.pwdb.keepasshttp.Message; -import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; import java.io.File; import java.io.FileNotFoundException; diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java index 9d40d54d..b578d062 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java @@ -19,7 +19,7 @@ import com.google.common.io.ByteStreams; import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.security.Encryption; -import org.spongycastle.util.encoders.Hex; +import org.bouncycastle.util.encoders.Hex; import java.io.IOException; import java.io.InputStream; diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockInputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockInputStream.java index 91a20d49..47d96986 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockInputStream.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockInputStream.java @@ -16,6 +16,8 @@ package org.linguafranca.pwdb.hashedblock; +import org.jetbrains.annotations.NotNull; + import java.io.ByteArrayInputStream; import java.io.EOFException; import java.io.IOException; @@ -24,8 +26,6 @@ import java.security.NoSuchAlgorithmException; import java.util.Arrays; -import org.jetbrains.annotations.NotNull; - /** * Takes an underlying input stream formatted as Hashed Blocks * and provides the content of the blocks as an input stream @@ -61,6 +61,7 @@ public class HashedBlockInputStream extends InputStream { /** * Create a Big Endian Hash Block Input Stream + * * @param inputStream the input stream containing the hash blocks */ public HashedBlockInputStream(InputStream inputStream) { @@ -69,17 +70,17 @@ public HashedBlockInputStream(InputStream inputStream) { /** * Create a Hash Block Input Stream with choice of endian encoding - * @param inputStream the input stream containing the hash blocks + * + * @param inputStream the input stream containing the hash blocks * @param littleEndian true if the stream is little endian encoded */ public HashedBlockInputStream(InputStream inputStream, boolean littleEndian) { this.inputStream = inputStream; this.littleEndian = littleEndian; try { - sha256 = MessageDigest.getInstance( "SHA-256" ); - } - catch( NoSuchAlgorithmException e ) { - throw new IllegalStateException( e ); + sha256 = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); } } @@ -104,7 +105,8 @@ public void close() throws IOException { /** * Gets bytes from the internal buffer and replenishes the buffer as necessary - * @param b a byte array to fill + * + * @param b a byte array to fill * @param offset the offset to strat from * @param length the number of bytes to return * @return the number of bytes actually returned, , -1 if end of file @@ -130,6 +132,7 @@ protected int get(byte[] b, int offset, int length) throws IOException { /** * Reload the internal buffer from the underlying input stream + * * @throws IOException */ protected void load() throws IOException { @@ -173,6 +176,7 @@ protected void load() throws IOException { /** * Read an unsigned 4 byte int decoding from the endian format + * * @return a long holding the value read * @throws IOException */ @@ -187,6 +191,7 @@ private long readUInt() throws IOException { /** * Fill the buffer passed + * * @param buffer the buffer to fill * @throws IOException if the buffer could not be filled */ diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockOutputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockOutputStream.java index a7a94f77..70a6c4be 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockOutputStream.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockOutputStream.java @@ -16,6 +16,8 @@ package org.linguafranca.pwdb.hashedblock; +import org.jetbrains.annotations.NotNull; + import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.IOException; @@ -23,8 +25,6 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import org.jetbrains.annotations.NotNull; - /** * Takes a stream of data and formats as Hashed Blocks to the underlying output stream. * @@ -60,6 +60,7 @@ public class HashedBlockOutputStream extends OutputStream { /** * Create a BigEndian Hash Block Output Stream + * * @param outputStream the output stream to receive the hash blocks */ public HashedBlockOutputStream(OutputStream outputStream) { @@ -68,19 +69,17 @@ public HashedBlockOutputStream(OutputStream outputStream) { /** * Create a Hash Block Output Stream with choice of endian encoding + * * @param outputStream the output stream to receive the hash blocks * @param littleEndian true to encode in a little endian way */ public HashedBlockOutputStream(OutputStream outputStream, boolean littleEndian) { this.outputStream = outputStream; this.littleEndian = littleEndian; - try - { - sha256 = MessageDigest.getInstance( "SHA-256" ); - } - catch( NoSuchAlgorithmException e ) - { - throw new IllegalStateException( e ); + try { + sha256 = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); } } @@ -119,7 +118,8 @@ public void close() throws IOException { /** * Writes to the internal buffer, and writes to the underlying output stream * as necessary as {@link #BLOCK_SIZE} blocks - * @param b the byte array to write + * + * @param b the byte array to write * @param offset offset in the byte array * @param length number of bytes to write * @throws IOException @@ -141,6 +141,7 @@ protected void put(byte[] b, int offset, int length) throws IOException { /** * Saqve the internal buffer to the underlying stream as a hash block + * * @throws IOException */ protected void save() throws IOException { @@ -171,6 +172,7 @@ protected void save() throws IOException { /** * Write a 4 byte int value to the underlying stream in appropriate endian format + * * @param value the value to write * @throws IOException */ @@ -179,6 +181,7 @@ protected void writeInt(int value) throws IOException { if (littleEndian) { output = Integer.reverseBytes(value); } - outputStream.write(new byte[]{(byte) (output >> 24), (byte) (output >> 16), (byte) (output >> 8), (byte) output}); + outputStream.write(new byte[]{(byte) (output >> 24), (byte) (output >> 16), + (byte) (output >> 8), (byte) output}); } } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java index a49cfbe4..948469cf 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java @@ -16,6 +16,9 @@ package org.linguafranca.pwdb.kdbx; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.macs.HMac; +import org.bouncycastle.crypto.params.KeyParameter; import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.security.*; @@ -174,15 +177,17 @@ public void verifyHeaderHmac(byte[] key, byte[] bytes) { throw new IllegalStateException("Header HMAC does not match"); } } - - // Alternative implementation of above using bouncy castle - /* +/* + // Alternative implementation of above using bouncy castle HMac hmac = new HMac(new SHA256Digest()); - hmac.init(new KeyParameter(hmacKey64)); - hmac.update(kdbxHeader.getHeaderBytes(), 0, kdbxHeader.getHeaderBytes().length); + hmac.init(new KeyParameter(key)); + hmac.update(bytes, 0, bytes.length); byte[] computedHmacSha256 = new byte[32]; hmac.doFinal(computedHmacSha256, 0); - */ + if (!Arrays.equals(computedHmacSha256, bytes)) { + throw new IllegalStateException("Header HMAC does not match"); + } + }*/ /** * Create a decrypted input stream using supplied digest and this header diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamEncryptor.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamEncryptor.java index 15f791c2..02c68c88 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamEncryptor.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamEncryptor.java @@ -17,12 +17,12 @@ package org.linguafranca.pwdb.kdbx; import org.linguafranca.pwdb.security.Encryption; -import org.spongycastle.crypto.StreamCipher; -import org.spongycastle.crypto.engines.ChaCha7539Engine; -import org.spongycastle.crypto.engines.Salsa20Engine; -import org.spongycastle.crypto.params.KeyParameter; -import org.spongycastle.crypto.params.ParametersWithIV; -import org.spongycastle.util.encoders.Hex; +import org.bouncycastle.crypto.StreamCipher; +import org.bouncycastle.crypto.engines.ChaCha7539Engine; +import org.bouncycastle.crypto.engines.Salsa20Engine; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; +import org.bouncycastle.util.encoders.Hex; import java.security.MessageDigest; diff --git a/pom.xml b/pom.xml index f3fab033..7598a3d4 100644 --- a/pom.xml +++ b/pom.xml @@ -37,7 +37,7 @@ pom KeePassJava2 - A Java 7 API for databases compatible with the renowned KeePass password safe for Windows + A Java 8 API for databases compatible with the renowned KeePass password safe for Windows https://github.com/jorabin/KeePassJava2 @@ -72,15 +72,15 @@ UTF-8 - 1.7 + 1.8 - 3.3 - 2.19 - 1.6.7 - 2.4 - 2.10.3 - 1.6 - 2.5 + 3.11.0 + 3.0.0 + 1.6.9 + 3.2.1 + 3.5.0 + 3.0.1 + 3.0.0 3.0.2 @@ -141,49 +141,48 @@ org.jetbrains annotations - 15.0 + 24.0.1 com.google.guava guava - 24.0-android + 31.1-android - - com.madgag.spongycastle - core - 1.58.0.0 + org.bouncycastle + bcpkix-jdk15on + 1.70 com.google.code.gson gson - 2.8.2 + 2.10.1 org.slf4j slf4j-api - 1.7.25 + 1.7.36 ch.qos.logback logback-core - 1.2.3 + 1.4.7 ch.qos.logback logback-classic - 1.2.3 + 1.4.7 commons-codec commons-codec - 1.11 + 1.15 junit junit - 4.12 + 4.13.2 diff --git a/simple/pom.xml b/simple/pom.xml index f34c6bab..504a9155 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -46,19 +46,19 @@ test - org.simpleframework - simple-xml + com.carrotsearch.thirdparty + simple-xml-safe 2.7.1 org.apache.httpcomponents httpcore - 4.4.5 + 4.4.15 com.fasterxml aalto-xml - 1.0.0 + 1.3.2 \ No newline at end of file From cc86a304cb5fbbbe396fb1716109ef42f36a5a10 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Thu, 4 May 2023 10:01:53 +0100 Subject: [PATCH 081/228] Preliminary working round trip save/load V4 kdbx / argon --- .gitignore | 1 + .../org/linguafranca/pwdb/security/Aes.java | 2 +- .../linguafranca/pwdb/security/Argon2.java | 15 ++ .../pwdb/security/Encryption.java | 5 + .../pwdb/security/KeyDerivationFunction.java | 7 +- .../pwdb/security/VariantDictionary.java | 21 ++- .../hashedblock/CollectingInputStream.java | 2 +- .../hashedblock/CollectingOutputStream.java | 77 ++++++++ .../hashedblock/HashedBlockOutputStream.java | 2 +- .../hashedblock/HmacBlockInputStream.java | 2 +- .../hashedblock/HmacBlockOutputStream.java | 152 ++++++++++++++++ .../linguafranca/pwdb/kdbx/KdbxHeader.java | 119 +++++++++---- .../pwdb/kdbx/KdbxSerializer.java | 167 +++++++++++++----- .../pwdb/kdbx/SerializableDatabase.java | 2 +- .../pwdb/kdbx/KdbxSerializerTest.java | 19 ++ 15 files changed, 512 insertions(+), 81 deletions(-) create mode 100644 kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingOutputStream.java create mode 100644 kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockOutputStream.java diff --git a/.gitignore b/.gitignore index 0e9e51f2..c4a6ad20 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ gradlew.bat gradle *.versionsBackup */target +/KeePass-2.53.1-Source/ diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java index ba6a867f..223136b6 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java @@ -55,7 +55,7 @@ public static Aes getInstance () { * get a copy of the Aes Variant dictionary * @return a copy */ - public static VariantDictionary createKdfParameters() { + public VariantDictionary createKdfParameters() { return kdfParameters.copy(); } diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java b/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java index 25b67f81..47646509 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java @@ -1,6 +1,7 @@ package org.linguafranca.pwdb.security; import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; import java.util.UUID; import static org.linguafranca.pwdb.security.Argon2.VariantDictionaryKeys.*; @@ -75,4 +76,18 @@ public byte[] getTransformedKey(byte[] digest, VariantDictionary argonParameterK gen.generateBytes(digest, result, 0, result.length); return result; } + + static final SecureRandom random = new SecureRandom(); + + @Override + public VariantDictionary createKdfParameters() { + VariantDictionary vd = new VariantDictionary((short) 1); + vd.putInt("P", 2); + vd.putInt("V", 19); + vd.putLong("I", 2); + vd.putLong("M", 64 * 1024 * 1024); + vd.putUuid("$UUID", Argon2.argon2_kdf); + vd.put("S", VariantDictionary.EntryType.ARRRAY, random.generateSeed(32)); + return vd; + } } diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java b/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java index 60c94e99..f99cfd62 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java @@ -127,6 +127,11 @@ public UUID getKdfUuid() { public byte[] getTransformedKey(byte[] key, VariantDictionary transformParams) { return kdf.getTransformedKey(key, transformParams); } + + @Override + public VariantDictionary createKdfParameters() { + return kdf.createKdfParameters(); + } } /** diff --git a/database/src/main/java/org/linguafranca/pwdb/security/KeyDerivationFunction.java b/database/src/main/java/org/linguafranca/pwdb/security/KeyDerivationFunction.java index 408ead7d..d0d928bc 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/KeyDerivationFunction.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/KeyDerivationFunction.java @@ -15,8 +15,13 @@ public interface KeyDerivationFunction { * Transform a key using this key derivation faunction * * @param key the key to transform - * @param transformParams the parameters defining the way the tranformation is to be carried out + * @param transformParams the parameters defining the way the transformation is to be carried out * @return a transformed key */ byte[] getTransformedKey(byte[] key, VariantDictionary transformParams); + + /** + * Create default KDF parameters + */ + VariantDictionary createKdfParameters(); } diff --git a/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java b/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java index 629cac52..85ed015c 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java @@ -11,8 +11,7 @@ import java.util.UUID; import static com.google.common.base.Preconditions.checkNotNull; -import static org.linguafranca.pwdb.security.VariantDictionary.EntryType.ARRRAY; -import static org.linguafranca.pwdb.security.VariantDictionary.EntryType.UINT64; +import static org.linguafranca.pwdb.security.VariantDictionary.EntryType.*; /** * Implementation of a storage for V4 KDBX Header field parameters @@ -128,6 +127,13 @@ public VariantDictionary copy() { return null; } + /** + * get the entries in this dictionary + */ + public Map getEntries(){ + return entries; + } + /** * Get the version number of this structure * @@ -191,12 +197,19 @@ public void putByteArray(@NotNull String key, @NotNull byte[] value) { } /** - * Put a long as an unsigned64 undewr the key defined + * Put a long as an signed64 under the key defined */ public void putLong(@NotNull String key, long value) { byte[] buf = new byte[8]; ByteBuffer bb = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN); bb.putLong(value); - entries.put(checkNotNull(key, knn), new Entry(UINT64, buf)); + entries.put(checkNotNull(key, knn), new Entry(INT64, buf)); + } + + public void putInt(@NotNull String key, int value) { + byte[] buf = new byte[4]; + ByteBuffer bb = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN); + bb.putInt(value); + entries.put(checkNotNull(key, knn), new Entry(INT32, buf)); } } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingInputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingInputStream.java index e932f62c..e301e625 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingInputStream.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingInputStream.java @@ -23,7 +23,7 @@ public class CollectingInputStream extends FilterInputStream { * * @param in the input stream to forward/collect */ - protected CollectingInputStream(InputStream in) { + public CollectingInputStream(InputStream in) { this(in, true); } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingOutputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingOutputStream.java new file mode 100644 index 00000000..90919b20 --- /dev/null +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingOutputStream.java @@ -0,0 +1,77 @@ +package org.linguafranca.pwdb.hashedblock; + +import org.jetbrains.annotations.NotNull; + +import java.io.*; + +/** + * Class allows the forwarding (as a filter) and collection of written bytes as a buffer + * - e.g. to provide for HMAC operations + * + * @author jo + */ +public class CollectingOutputStream extends FilterOutputStream { + + private final ByteArrayOutputStream collectedBytes = new ByteArrayOutputStream(); + private boolean collecting = true; + + /** + * Create a collecting stream which is set to collect from the get go + * + * @param out the output stream to forward/collect + */ + public CollectingOutputStream(OutputStream out) { + this(out, true); + } + + /** + * Create a collecting stream + * + * @param out the output stream to forward/collect + * @param collecting whether the initial state is collecting or not + */ + public CollectingOutputStream(OutputStream out, boolean collecting) { + super(out); + this.collecting = collecting; + } + + @Override + public void write(int toWrite) throws IOException { + super.write(toWrite); + if (collecting) { + collectedBytes.write(toWrite); + } + } + + @Override + public void write(byte @NotNull [] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte @NotNull [] b, int off, int len) throws IOException { + super.write(b, off, len); + } + + /** + * Get the collected bytes as a byte array + */ + public byte[] getCollectedBytes() { + return collectedBytes.toByteArray(); + } + + /** + * True if we are currently collecting bytes + */ + public boolean isCollecting() { + return collecting; + } + + /** + * Cganeg the state of collecting bytes + * @param collecting true to collect + */ + public void setCollecting(boolean collecting) { + this.collecting = collecting; + } +} diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockOutputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockOutputStream.java index 70a6c4be..e376e556 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockOutputStream.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockOutputStream.java @@ -140,7 +140,7 @@ protected void put(byte[] b, int offset, int length) throws IOException { } /** - * Saqve the internal buffer to the underlying stream as a hash block + * Save the internal buffer to the underlying stream as a hash block * * @throws IOException */ diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java index 07bbd1c2..9a7825df 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java @@ -99,7 +99,7 @@ private void getBlock() throws IOException { * @param hmacSha256 the hmac to verify */ private void verifyHmac(byte[] buffer, long blockNumber, byte[] hmacSha256) { - final byte[] transformedKey = transformHmacKey(this.key, toBytes(blockNumber, ByteOrder.LITTLE_ENDIAN)); + final byte[] transformedKey = transformHmacKey(this.key, toBytes(blockNumber, byteOrder)); final Mac mac = getHMacSha256Instance(transformedKey); mac.update(toBytes(blockNumber, byteOrder)); mac.update(toBytes(buffer.length, byteOrder)); diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockOutputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockOutputStream.java new file mode 100644 index 00000000..5dd0d252 --- /dev/null +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockOutputStream.java @@ -0,0 +1,152 @@ +package org.linguafranca.pwdb.hashedblock; + +import static org.linguafranca.pwdb.kdbx.Helpers.toBytes; +import static org.linguafranca.pwdb.security.Encryption.getHMacSha256Instance; +import static org.linguafranca.pwdb.security.Encryption.transformHmacKey; + +import java.io.*; +import java.nio.ByteOrder; +import javax.crypto.Mac; +import org.jetbrains.annotations.NotNull; +import com.google.common.io.LittleEndianDataOutputStream; + +/** + * Takes a stream of (encrypted GZIPped) data and formats it as HMAC Hashed Blocks to the + * underlying output stream. + *

    + * An HMac block consists of + *

      + *
    1. a 32 byte HMac checksum
    2. + *
    3. a 4 byte block size
    4. + *
    5. {blockSize} bytes of data
    6. + *
    + *

    + * The Class is initialized with an initial key digest. For each block this key is transformed + * using the (implied, starting from 0) block number and used as the key for the block verification process. + * That process consists of digesting the block number, its length, and its content. + *

    + * Streams in Keepass are Little Endian. + */ + +public class HmacBlockOutputStream extends FilterOutputStream { + private static final int BLOCK_SIZE = (int) Math.pow(2, 20); // blocks can technically be bigger but KeePass supposedly splits them into this size + + private final byte[] key; + + private final ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(); + /** the encrypted, compressed output stream */ + private final DataOutput output; + private final ByteOrder byteOrder; + private long nextSequenceNumber = 0; + private boolean isClosed = false; + + /** + * Create a BigEndian HmacBlockOutputStream + * @param key credentials for this database + * @param outputStream the output stream to receive the hash blocks + */ + public HmacBlockOutputStream(byte [] key, OutputStream outputStream) throws IOException { + this(key, outputStream, false); + } + + /** + * Create a HmacBlockOutputStream with choice of endian encoding + * @param key credentials for this database + * @param outputStream the output stream to receive the hash blocks + * @param littleEndian true to encode in a little endian way + */ + public HmacBlockOutputStream(byte [] key, OutputStream outputStream, boolean littleEndian) throws IOException { + super(outputStream); + this.key = key; + this.byteOrder = littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN; + if (this.byteOrder.equals(ByteOrder.LITTLE_ENDIAN)) { + //noinspection UnstableApiUsage + this.output = new LittleEndianDataOutputStream(out); + } else { + this.output = new DataOutputStream(out); + } + } + + @Override + public void close() throws IOException { + if (isClosed) { + throw new EOFException(); + } + // clear unwritten data + flush(); + // write final block + writeBlock(new byte [0]); + // push the contents to disk etc. + out.flush(); + out.close(); + isClosed = true; + } + + private void writeBlock(byte [] buffer) throws IOException { + final byte[] transformedKey = transformHmacKey(this.key, toBytes(nextSequenceNumber, byteOrder)); + final Mac mac = getHMacSha256Instance(transformedKey); + mac.update(toBytes(nextSequenceNumber++, byteOrder)); + mac.update(toBytes(buffer.length, byteOrder)); + mac.update(buffer); + byte[] finalMac = mac.doFinal(); + output.write(finalMac); + + // write the buffer's length + output.writeInt(buffer.length); + output.write(buffer); + out.flush(); + } + + /** + * Writes to the internal buffer, and writes to the underlying output stream + * as necessary as {@link #BLOCK_SIZE} blocks + * @param b the byte array to write + * @param offset offset in the byte array + * @param length number of bytes to write + */ + protected void put(byte[] b, int offset, int length) throws IOException { + if (isClosed) { + throw new EOFException(); + } + + while (length > 0) { + int bytesToWrite = Math.min(BLOCK_SIZE - outputBuffer.size(), length); + outputBuffer.write(b, offset, bytesToWrite); + + if (outputBuffer.size() >= BLOCK_SIZE) { + save(); + } + + offset += bytesToWrite; + length -= bytesToWrite; + } + } + + /** + * Save the internal buffer to the underlying stream as a Hmac block + */ + protected void save() throws IOException { + if (outputBuffer.size() == 0) { + return; + } + writeBlock(outputBuffer.toByteArray()); + outputBuffer.reset(); + } + + @Override + public void write(int i) throws IOException { + byte[] buf = new byte[1]; + buf[0] = (byte) i; + put(buf, 0, 1); + } + + @Override + public void write(byte @NotNull [] b, int offset, int count) throws IOException { + put(b, offset, count); + } + + @Override + public void flush() throws IOException { + save(); + } +} \ No newline at end of file diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java index 948469cf..f914d8f1 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java @@ -16,9 +16,6 @@ package org.linguafranca.pwdb.kdbx; -import org.bouncycastle.crypto.digests.SHA256Digest; -import org.bouncycastle.crypto.macs.HMac; -import org.bouncycastle.crypto.params.KeyParameter; import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.security.*; @@ -38,10 +35,13 @@ /** * This class represents the header portion of a KeePass KDBX file or stream. The header is received in * plain text and describes the encryption and compression of the remainder of the file. - *

    - *

    It is a factory for encryption and decryption streams. It provides for verification of its own serialization. - *

    - *

    While KDBX streams are Little-Endian, data is passed to and from this class in standard Java byte order. + *

    + * In V4 the header became Outer Header and Inner Header. The class stores the configuration + * contents of both and binary attachments. + *

    + * It is a factory for encryption and decryption streams. It provides for verification of its own serialization. + *

    + * While KDBX streams are Little-Endian, data is passed to and from this class in standard Java byte order. */ @SuppressWarnings("WeakerAccess") public class KdbxHeader { @@ -65,7 +65,7 @@ public enum CompressionFlags { public enum ProtectedStreamAlgorithm { NONE(0), ARC_FOUR(1), SALSA_20(2), CHA_CHA_20(3); - private int value; + private final int value; ProtectedStreamAlgorithm(int value) { this.value = value; @@ -81,10 +81,10 @@ public static ProtectedStreamAlgorithm getAlgorithm(int innerRandomStreamId) { } } - private List allowableVersions = new ArrayList<>(Arrays.asList(3, 4)); + private final List allowableVersions = new ArrayList<>(Arrays.asList(3, 4)); - /* version of the file */ + /* version of the file - most significant 2 bytes i.e. 0x0302 is version 3 */ private int version; protected UUID cipherUuid; @@ -125,17 +125,78 @@ public static ProtectedStreamAlgorithm getAlgorithm(int innerRandomStreamId) { private byte[] headerBytes; /** - * Construct a default KDBX header + * Provides for choice of version number and crypto options for V3 and V4 */ - public KdbxHeader() { - this(3); + interface KdbxHeaderOptions { + int getVersion(); + CipherAlgorithm getCipherAlgorithm(); + KeyDerivationFunction getKeyDerivationFunction(); + ProtectedStreamAlgorithm getProtectedStreamAlgorithm(); } + /** + * Default values for crypto options + */ + enum KdbxHeaderOpts implements KdbxHeaderOptions{ + V3_AES_SALSA_20(3, Encryption.Cipher.AES, Encryption.Kdf.AES, ProtectedStreamAlgorithm.SALSA_20), + V4_AES_ARGON_CHA_CHA (4, Encryption.Cipher.AES, Encryption.Kdf.ARGON2, ProtectedStreamAlgorithm.CHA_CHA_20); + + // + final int version; + final CipherAlgorithm algorithm; + final KeyDerivationFunction kdf; + final ProtectedStreamAlgorithm protectedStreamAlgorithm; + + + KdbxHeaderOpts(int version, Encryption.Cipher cipher, Encryption.Kdf kdf, ProtectedStreamAlgorithm protectedStreamAlgorithm) { + this.version = version; + this.algorithm = cipher; + this.kdf = kdf; + this.protectedStreamAlgorithm = protectedStreamAlgorithm; + } + + @Override + public int getVersion() { + return version; + } + + @Override + public CipherAlgorithm getCipherAlgorithm() { + return algorithm; + } + + @Override + public KeyDerivationFunction getKeyDerivationFunction() { + return kdf; + } + + @Override + public ProtectedStreamAlgorithm getProtectedStreamAlgorithm() { + return protectedStreamAlgorithm; + } + // + } + + /** + * Construct a default version 3 KDBX header + */ + public KdbxHeader(){ + this(KdbxHeaderOpts.V3_AES_SALSA_20); + } + + /** + * Construct a default KDBX header with AES/AES/SALSA_20 + */ public KdbxHeader(int version) { + this(version ==3 ? KdbxHeaderOpts.V3_AES_SALSA_20 : KdbxHeaderOpts.V4_AES_ARGON_CHA_CHA); + } + + + public KdbxHeader(KdbxHeaderOptions opts) { SecureRandom random = new SecureRandom(); - this.version = version; - cipherUuid = Aes.getInstance().getCipherUuid(); + this.version = opts.getVersion(); + cipherUuid = opts.getCipherAlgorithm().getCipherUuid(); compressionFlags = CompressionFlags.GZIP; masterSeed = random.generateSeed(32); transformSeed = random.generateSeed(32); @@ -143,9 +204,9 @@ public KdbxHeader(int version) { encryptionIv = random.generateSeed(16); innerRandomStreamKey = random.generateSeed(32); streamStartBytes = new byte[32]; - protectedStreamAlgorithm = ProtectedStreamAlgorithm.SALSA_20; + this.protectedStreamAlgorithm = opts.getProtectedStreamAlgorithm(); - kdfParameters = Aes.createKdfParameters(); + kdfParameters = opts.getKeyDerivationFunction().createKdfParameters(); } /** @@ -177,17 +238,6 @@ public void verifyHeaderHmac(byte[] key, byte[] bytes) { throw new IllegalStateException("Header HMAC does not match"); } } -/* - // Alternative implementation of above using bouncy castle - HMac hmac = new HMac(new SHA256Digest()); - hmac.init(new KeyParameter(key)); - hmac.update(bytes, 0, bytes.length); - byte[] computedHmacSha256 = new byte[32]; - hmac.doFinal(computedHmacSha256, 0); - if (!Arrays.equals(computedHmacSha256, bytes)) { - throw new IllegalStateException("Header HMAC does not match"); - } - }*/ /** * Create a decrypted input stream using supplied digest and this header @@ -227,7 +277,7 @@ public byte[] getTransformedKeyDigest(byte[] digest) { } /** - * Create an unencrypted outputstream using the supplied digest and this header + * Create an unencrypted outputStream using the supplied digest and this header * and use the supplied output stream to write encrypted data. * * @param digest the key digest @@ -242,6 +292,7 @@ public OutputStream createEncryptedStream(byte[] digest, OutputStream outputStre return Aes.getInstance().getEncryptedOutputStream(outputStream, finalKeyDigest, getEncryptionIv()); } + // public byte[] getTransformSeed() { if (version < 4) { return transformSeed; @@ -292,6 +343,11 @@ public int getVersion() { return version; } + // V4 + public VariantDictionary getKdfParameters() { + return kdfParameters; + } + public StreamEncryptor getStreamEncryptor() { switch (getProtectedStreamAlgorithm()) { case NONE: { @@ -350,14 +406,14 @@ public void setCipherUuid(byte[] uuid) { ByteBuffer b = ByteBuffer.wrap(uuid); UUID incoming = new UUID(b.getLong(), b.getLong(8)); if (!incoming.equals(Aes.getInstance().getCipherUuid()) && !incoming.equals(ChaCha.getInstance().getCipherUuid())) { - throw new IllegalStateException("Unknown Cipher UUID " + incoming.toString()); + throw new IllegalStateException("Unknown Cipher UUID " + incoming); } this.cipherUuid = incoming; } public void setVersion(int version) { if (!allowableVersions.contains(version)) { - throw new IllegalStateException("File version must be in " + allowableVersions.toString()); + throw new IllegalStateException("File version must be in " + allowableVersions); } this.version = version; } @@ -402,4 +458,5 @@ public void setHeaderBytes(byte[] headerBytes) { System.arraycopy(headerBytes, 0, copy, 0, headerBytes.length); this.headerBytes = copy; } + // } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java index c910e13a..d0ce5d90 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java @@ -19,27 +19,28 @@ import com.google.common.io.LittleEndianDataInputStream; import com.google.common.io.LittleEndianDataOutputStream; import org.linguafranca.pwdb.Credentials; -import org.linguafranca.pwdb.hashedblock.CollectingInputStream; -import org.linguafranca.pwdb.hashedblock.HashedBlockInputStream; -import org.linguafranca.pwdb.hashedblock.HashedBlockOutputStream; -import org.linguafranca.pwdb.hashedblock.HmacBlockInputStream; +import org.linguafranca.pwdb.hashedblock.*; import org.linguafranca.pwdb.security.Encryption; import org.linguafranca.pwdb.security.VariantDictionary; +import javax.crypto.Mac; import java.io.*; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; import java.security.DigestInputStream; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.util.Arrays; +import java.util.Map; +import java.util.function.IntConsumer; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; /** * This class provides static methods for the encryption and decryption of Keepass KDBX V3 and V4 files. *

    - * A KDBX files are little-endian and consist of the following: + * KDBX files are little-endian and consist of the following: *

      *
    1. An unencrypted portion
    2. *
        @@ -61,7 +62,7 @@ *
      1. The content is now a character stream, which is expected to be * XML representing a KeePass Database. Assumed UTF-8 encoding.
      2. *
      - *
    3. In V4 the remainder of the file is encoded as HMacHasedBlocks:
    4. + *
    5. In V4 the remainder of the file is encoded as HMacBlocks:
    6. *
        *
      1. A sequence of blocks encoded using Hmac Blocks see {@link HmacBlockInputStream}
      2. *
      3. Those blocks contain an encrypted input stream.
      4. @@ -80,7 +81,7 @@ * @see
        this diagram * @author jo */ -@SuppressWarnings("WeakerAccess") +@SuppressWarnings({"WeakerAccess", "UnstableApiUsage"}) public class KdbxSerializer { // make entirely static @@ -130,7 +131,7 @@ public static InputStream createUnencryptedInputStream(Credentials credentials, /** * Provides an {@link OutputStream} to be encoded and encrypted in KDBX format - * // TODO only writes in V3 format + * * @param credentials credentials for encryption of the stream * @param kdbxHeader a KDBX header to control the formatting and encryption operation * @param outputStream output stream to contain the KDBX formatted output @@ -141,18 +142,34 @@ public static OutputStream createEncryptedOutputStream(Credentials credentials, writeKdbxHeader(kdbxHeader, outputStream); - OutputStream encryptedOutputStream = kdbxHeader.createEncryptedStream(credentials.getKey(), outputStream); + OutputStream result; - writeStartBytes(kdbxHeader, encryptedOutputStream); + if (kdbxHeader.getVersion() >= 4) { + // write in plaintext + writeOuterHeaderVerification(kdbxHeader, credentials, new DataOutputStream(outputStream)); + // write encrypted blocks + OutputStream blockOutputStream = new HmacBlockOutputStream(kdbxHeader.getHmacKey(credentials), outputStream, true); + // accept plaintext + result = kdbxHeader.createEncryptedStream(credentials.getKey(), blockOutputStream); - HashedBlockOutputStream blockOutputStream = new HashedBlockOutputStream(encryptedOutputStream, true); + } else { + OutputStream encryptedOutputStream = kdbxHeader.createEncryptedStream(credentials.getKey(), outputStream); + // write encrypted + writeStartBytes(kdbxHeader, encryptedOutputStream); + // input is plaintext, output is encrypted after + result = new HashedBlockOutputStream(encryptedOutputStream, true); + } - if(kdbxHeader.getCompressionFlags().equals(KdbxHeader.CompressionFlags.NONE)) { - return blockOutputStream; + if(kdbxHeader.getCompressionFlags().equals(KdbxHeader.CompressionFlags.GZIP)) { + result = new GZIPOutputStream(result); } - return new GZIPOutputStream(blockOutputStream); - } + if (kdbxHeader.getVersion() >= 4) { + writeInnerHeader(kdbxHeader, result); + } + + return result; + } /** * Checks that the decrypted stream starts with the expected bytes in V3 format @@ -173,7 +190,7 @@ private static void checkStartBytes(KdbxHeader kdbxHeader, InputStream decrypted /** * Writes the expected stream start bytes to the encrypted stream for V3 format * @param kdbxHeader the header - * @param encryptedOutputStream the encypted stream + * @param encryptedOutputStream the encrypted stream * @throws IOException if the stream cannot be written, etc. */ private static void writeStartBytes(KdbxHeader kdbxHeader, OutputStream encryptedOutputStream) throws IOException { @@ -183,7 +200,6 @@ private static void writeStartBytes(KdbxHeader kdbxHeader, OutputStream encrypte private static final int SIG1 = 0x9AA2D903; private static final int SIG2 = 0xB54BFB67; - private static final int FILE_VERSION_CRITICAL_MASK = 0xFFFF0000; private static final int FILE_VERSION_32 = 0x00030001; private static final int FILE_VERSION_4 = 0x00040000; @@ -202,7 +218,7 @@ private static class HeaderType { static final byte KDF_PARAMETERS = 11; static final byte CUSTOM_DATA = 12; } - + /** * Read two lots of 4 bytes and verify that they satisfy the signature of a kdbx file; * @param ledis an input stream @@ -250,7 +266,7 @@ public static KdbxHeader readOuterHeader(InputStream inputStream, KdbxHeader kdb } /** - * V4 header is followed by an SHA256 and then contains an HMACSHA256 after that. + * V4 header is followed by an SHA256 and then contains an HMAC SHA256 after that. * @param kdbxHeader the header containing the relevant parameters * @param credentials the credentials - used to verify the HMAC * @param input an input source @@ -272,6 +288,16 @@ public static void verifyOuterHeader(KdbxHeader kdbxHeader, Credentials credenti kdbxHeader.verifyHeaderHmac(hmacKey64, getBytes(32, input)); } + private static void writeOuterHeaderVerification(KdbxHeader kdbxHeader, Credentials credentials, DataOutputStream dataOutputStream) throws IOException { + dataOutputStream.write(kdbxHeader.getHeaderHash()); + + byte[] hmacKey = kdbxHeader.getHmacKey(credentials); + byte[] hmacKey64 = Encryption.transformHmacKey(hmacKey, Helpers.toBytes(-1L, ByteOrder.LITTLE_ENDIAN)); + Mac mac = Encryption.getHMacSha256Instance(hmacKey64); + byte[] hashedHeaderBytes = mac.doFinal(kdbxHeader.getHeaderBytes()); + dataOutputStream.write(hashedHeaderBytes); + } + private static void getOuterHeaderFields(KdbxHeader kdbxHeader, MessageDigest digest, DataInput input) throws IOException { byte headerType; do { @@ -341,7 +367,7 @@ private static void getOuterHeaderFields(KdbxHeader kdbxHeader, MessageDigest di } /** - * Type fieds for inner headers + * Type fields for inner headers * @see KdbxSerializer#readInnerHeader */ private static class InnerHeaderType { @@ -392,6 +418,21 @@ private static void readInnerHeader(KdbxHeader kdbxHeader, InputStream plainText } while (headerType != HeaderType.END); } + public static void writeInnerHeader(KdbxHeader kdbxHeader, OutputStream outputStream) throws IOException { + DataOutput output = new LittleEndianDataOutputStream(outputStream); + + output.writeByte(InnerHeaderType.INNER_RANDOM_STREAM_ID); + output.writeInt(4); + output.writeInt(kdbxHeader.getProtectedStreamAlgorithm().ordinal()); + + output.writeByte(InnerHeaderType.INNER_RANDOM_STREAM_KEY); + output.writeInt(kdbxHeader.getInnerRandomStreamKey().length); + output.write(kdbxHeader.getInnerRandomStreamKey()); + + output.writeByte(InnerHeaderType.END); + output.writeInt(0); + } + /** * Read a VariantDictionary from the supplied input * @param input source of data @@ -410,8 +451,8 @@ private static VariantDictionary makeVariantDictionary(int length, DataInput inp byte type = buf.get(); while (type != 0) { // get key - int keylength = buf.getInt(); - byte [] key = new byte[keylength]; + int keyLength = buf.getInt(); + byte [] key = new byte[keyLength]; buf.get(key); // get value @@ -420,34 +461,68 @@ private static VariantDictionary makeVariantDictionary(int length, DataInput inp buf.get(value); // add entry - vd.put(new String(key), VariantDictionary.EntryType.get(type), value); + vd.put(new String(key, StandardCharsets.US_ASCII), VariantDictionary.EntryType.get(type), value); type = buf.get(); } return vd; } + private static byte[] serializeVariantDictionary(VariantDictionary v) { + ByteBuffer buf = ByteBuffer.wrap(new byte[1024]); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.mark(); + // version number must be 0x01?? + buf.putShort((short) 0x100); //1 + for (Map.Entry e : v.getEntries().entrySet()){ + buf.put(e.getValue().getType()); + buf.putInt(e.getKey().length()); + buf.put(e.getKey().getBytes(StandardCharsets.US_ASCII)); + buf.putInt(e.getValue().asByteArray().length); + buf.put(e.getValue().asByteArray()); + } + buf.put((byte) 0); + byte[] result = new byte[buf.position()]; + buf.reset(); + buf.get(result); + return result; + } + /** * Write a KdbxHeader to the output stream supplied. The header is updated with the * message digest of the written stream. * @param kdbxHeader the header to write and update - * @param outputStream the output stream * @throws IOException on error */ + @SuppressWarnings("UnstableApiUsage") public static void writeKdbxHeader(KdbxHeader kdbxHeader, OutputStream outputStream) throws IOException { MessageDigest messageDigest = Encryption.getSha256MessageDigestInstance(); DigestOutputStream digestOutputStream = new DigestOutputStream(outputStream, messageDigest); - LittleEndianDataOutputStream ledos = new LittleEndianDataOutputStream(digestOutputStream); + CollectingOutputStream collectingOutputStream = new CollectingOutputStream(digestOutputStream); + LittleEndianDataOutputStream ledos = new LittleEndianDataOutputStream(collectingOutputStream); + + // lengths are short in v3 int in v4 + IntConsumer lengthWriter = (i -> { + try { + if (kdbxHeader.getVersion() == 3) { + ledos.writeShort(i); + } else { + ledos.writeInt(i); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); // write the magic number ledos.writeInt(SIG1); ledos.writeInt(SIG2); // write a file version - ledos.writeInt(FILE_VERSION_32); + ledos.writeInt(kdbxHeader.getVersion() == 3?FILE_VERSION_32:FILE_VERSION_4); ledos.writeByte(HeaderType.CIPHER_ID); - ledos.writeShort(16); + lengthWriter.accept(16); byte[] b = new byte[16]; ByteBuffer bb = ByteBuffer.wrap(b); bb.putLong(kdbxHeader.getCipherUuid().getMostSignificantBits()); @@ -455,42 +530,54 @@ public static void writeKdbxHeader(KdbxHeader kdbxHeader, OutputStream outputStr ledos.write(b); ledos.writeByte(HeaderType.COMPRESSION_FLAGS); - ledos.writeShort(4); + lengthWriter.accept(4); ledos.writeInt(kdbxHeader.getCompressionFlags().ordinal()); ledos.writeByte(HeaderType.MASTER_SEED); - ledos.writeShort(kdbxHeader.getMasterSeed().length); + lengthWriter.accept(kdbxHeader.getMasterSeed().length); ledos.write(kdbxHeader.getMasterSeed()); - ledos.writeByte(HeaderType.TRANSFORM_SEED); - ledos.writeShort(kdbxHeader.getTransformSeed().length); - ledos.write(kdbxHeader.getTransformSeed()); + if (kdbxHeader.getVersion() < 4) { + ledos.writeByte(HeaderType.TRANSFORM_SEED); + lengthWriter.accept(kdbxHeader.getTransformSeed().length); + ledos.write(kdbxHeader.getTransformSeed()); - ledos.writeByte(HeaderType.TRANSFORM_ROUNDS); - ledos.writeShort(8); - ledos.writeLong(kdbxHeader.getTransformRounds()); + ledos.writeByte(HeaderType.TRANSFORM_ROUNDS); + lengthWriter.accept(8); + ledos.writeLong(kdbxHeader.getTransformRounds()); + } ledos.writeByte(HeaderType.ENCRYPTION_IV); - ledos.writeShort(kdbxHeader.getEncryptionIv().length); + lengthWriter.accept(kdbxHeader.getEncryptionIv().length); ledos.write(kdbxHeader.getEncryptionIv()); ledos.writeByte(HeaderType.INNER_RANDOM_STREAM_KEY); - ledos.writeShort(kdbxHeader.getInnerRandomStreamKey().length); + lengthWriter.accept(kdbxHeader.getInnerRandomStreamKey().length); ledos.write(kdbxHeader.getInnerRandomStreamKey()); ledos.writeByte(HeaderType.STREAM_START_BYTES); - ledos.writeShort(kdbxHeader.getStreamStartBytes().length); + lengthWriter.accept(kdbxHeader.getStreamStartBytes().length); ledos.write(kdbxHeader.getStreamStartBytes()); ledos.writeByte(HeaderType.INNER_RANDOM_STREAM_ID); - ledos.writeShort(4); + lengthWriter.accept(4); ledos.writeInt(kdbxHeader.getProtectedStreamAlgorithm().ordinal()); + if (kdbxHeader.getVersion() > 3) { + ledos.writeByte(HeaderType.KDF_PARAMETERS); + byte [] vd = serializeVariantDictionary(kdbxHeader.getKdfParameters()); + lengthWriter.accept(vd.length); + ledos.write(vd); + } + ledos.writeByte(HeaderType.END); - ledos.writeShort(0); + lengthWriter.accept(0); MessageDigest digest = digestOutputStream.getMessageDigest(); kdbxHeader.setHeaderHash(digest.digest()); + + collectingOutputStream.setCollecting(false); + kdbxHeader.setHeaderBytes(collectingOutputStream.getCollectedBytes()); } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/SerializableDatabase.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/SerializableDatabase.java index 32fa5084..9ce4173a 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/SerializableDatabase.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/SerializableDatabase.java @@ -26,7 +26,7 @@ *

        Databases instantiate themselves from a stream and serialize to a stream, * and need to be able to encrypt and decrypt data (e.g. Protected fields in KDBX format). * - *

        KDBX databases contain a header hash (i.e. a hash of the contents of + *

        KDBX V3 databases contain a header hash (i.e. a hash of the contents of * some portion of the {@link StreamFormat} they have been loaded from or saved to. * Which means that databases must support the setting of this value after the header * has been written on save, and reading the value after load to allow for integrity checking. diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java index d3f686dd..e29aeb35 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java @@ -16,8 +16,10 @@ package org.linguafranca.pwdb.kdbx; +import org.bouncycastle.crypto.params.KDFParameters; import org.junit.Test; import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.security.VariantDictionary; import java.io.*; import java.util.Scanner; @@ -57,4 +59,21 @@ public void testCypherTextOutputStream() throws Exception { Scanner scanner = new Scanner(inputStream); assertEquals("Hello World", scanner.nextLine()); } + + @Test + public void testCypherTextOutputStream2() throws Exception { + File tempFile = File.createTempFile("test", "test"); + OutputStream testStream = new FileOutputStream(tempFile); + Credentials credentials = new KdbxCreds("123".getBytes()); + KdbxHeader kdbxHeader = new KdbxHeader(4); + OutputStream outputStream = KdbxSerializer.createEncryptedOutputStream(credentials, kdbxHeader, testStream); + + outputStream.write("Hello World\n".getBytes()); + outputStream.flush(); + outputStream.close(); + + InputStream inputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), new FileInputStream(tempFile)); + Scanner scanner = new Scanner(inputStream); + assertEquals("Hello World", scanner.nextLine()); + } } \ No newline at end of file From 58ac391984cca343359cacd4b8ad7186aa2b6849 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 5 May 2023 18:00:49 +0100 Subject: [PATCH 082/228] Tidy and add comments --- .../linguafranca/pwdb/security/ChaCha.java | 2 +- .../pwdb/security/CipherAlgorithm.java | 2 +- .../linguafranca/pwdb/kdbx/QuickStart.java | 10 +- .../pwdb/kdbx/KdbxSerializer.java | 284 ++++++++++-------- .../pwdb/kdbx/StreamEncryptor.java | 1 - .../linguafranca/pwdb/kdbx/HelperTest.java | 5 +- .../pwdb/kdbx/KdbxKeyFileTest.java | 57 ++-- .../pwdb/kdbx/KdbxSerializerTest.java | 15 +- .../pwdb/kdbx/stream/KdbxSerializerTest.java | 31 +- .../pwdb/security/StreamEncryptionTest.java | 1 + 10 files changed, 220 insertions(+), 188 deletions(-) diff --git a/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java b/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java index c9d5d710..2d475651 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java @@ -22,7 +22,7 @@ public class ChaCha implements CipherAlgorithm { // hide constructor to enforce singleton private ChaCha(){} - private static ChaCha instance = new ChaCha(); + private static final ChaCha instance = new ChaCha(); public static ChaCha getInstance() { return instance; diff --git a/database/src/main/java/org/linguafranca/pwdb/security/CipherAlgorithm.java b/database/src/main/java/org/linguafranca/pwdb/security/CipherAlgorithm.java index 970b78ef..db7f396b 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/CipherAlgorithm.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/CipherAlgorithm.java @@ -14,7 +14,7 @@ public interface CipherAlgorithm { UUID getCipherUuid(); /** - * Create a decrypted stream from the supplied encruypted one + * Create a decrypted stream from the supplied encrypted one * * @param encryptedInputStream an encryted stream * @param key the decryption key diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java index ef1fc94e..cde5c12a 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java @@ -130,14 +130,14 @@ public void loadKdb() throws IOException { database.visit(new Visitor.Print()); // create a KDBX (database - D kdbxDatabse = getDatabase(); - kdbxDatabse.setName("New Database"); - kdbxDatabse.setDescription("Migration of KDB Database to KDBX Database"); + D kdbxDatabase = getDatabase(); + kdbxDatabase.setName("New Database"); + kdbxDatabase.setDescription("Migration of KDB Database to KDBX Database"); // deep copy from group (not including source group, KDB database has simulated root) - kdbxDatabse.getRootGroup().copy(database.getRootGroup()); + kdbxDatabase.getRootGroup().copy(database.getRootGroup()); // save it try (FileOutputStream f = new FileOutputStream("testOutput/migration.kdbx")) { - kdbxDatabse.save(new KdbxCreds("123".getBytes()), f); + kdbxDatabase.save(new KdbxCreds("123".getBytes()), f); } } } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java index d0ce5d90..ab84988e 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java @@ -96,36 +96,36 @@ private KdbxSerializer() {} * @throws IOException on error */ public static InputStream createUnencryptedInputStream(Credentials credentials, KdbxHeader kdbxHeader, InputStream inputStream) throws IOException { - + // read plaintext outer header readOuterHeader(inputStream, kdbxHeader); InputStream plainTextStream; if (kdbxHeader.getVersion() >= 4) { - - verifyOuterHeader(kdbxHeader, credentials, new DataInputStream(inputStream)); - + // verify the header hashes + readOuterHeaderVerification(kdbxHeader, credentials, new DataInputStream(inputStream)); + // create an HMac Block input stream HmacBlockInputStream hmacBlockInputStream = new HmacBlockInputStream(kdbxHeader.getHmacKey(credentials), inputStream, true); - + // decrypt the contents of the blocks plainTextStream = kdbxHeader.createDecryptedStream(credentials.getKey(), hmacBlockInputStream); - } else { - + } else {// V3 + // decrypt the input stream InputStream decryptedInputStream = kdbxHeader.createDecryptedStream(credentials.getKey(), inputStream); - + // the unencrypted stream starts with a byte pattern checkStartBytes(kdbxHeader, decryptedInputStream); - + // unencrypted stream consists of hashed blocks plainTextStream = new HashedBlockInputStream(decryptedInputStream, true); } - + // need to decompress the stream if (kdbxHeader.getCompressionFlags().equals(KdbxHeader.CompressionFlags.GZIP)) { plainTextStream = new GZIPInputStream(plainTextStream); } - + // read inner header in V4 if (kdbxHeader.getVersion() >= 4) { readInnerHeader(kdbxHeader, plainTextStream); } - + // stream is now positioned on payload (assumed to be Keepass XML) return plainTextStream; } @@ -139,7 +139,7 @@ public static InputStream createUnencryptedInputStream(Credentials credentials, * @throws IOException on error */ public static OutputStream createEncryptedOutputStream(Credentials credentials, KdbxHeader kdbxHeader, OutputStream outputStream) throws IOException { - + // write plain text header writeKdbxHeader(kdbxHeader, outputStream); OutputStream result; @@ -219,18 +219,6 @@ private static class HeaderType { static final byte CUSTOM_DATA = 12; } - /** - * Read two lots of 4 bytes and verify that they satisfy the signature of a kdbx file; - * @param ledis an input stream - * @return true if it looks like this is a kdbx file - * @throws IOException on error - */ - private static boolean verifyMagicNumber(LittleEndianDataInputStream ledis) throws IOException { - int sig1 = ledis.readInt(); - int sig2 = ledis.readInt(); - return sig1 == SIG1 && sig2 == SIG2; - } - /** * Create and populate a KdbxHeader from the input stream supplied * @param inputStream an input stream @@ -242,7 +230,9 @@ public static KdbxHeader readOuterHeader(InputStream inputStream, KdbxHeader kdb // header is digested to verify correctness MessageDigest digest = Encryption.getSha256MessageDigestInstance(); DigestInputStream shaDigestInputStream = new DigestInputStream(inputStream, digest); - // collect the bytes of the header, we'll need them later + // collect the bytes of the header, we'll need them later for the HMAC header calculation + // we can't use a similar technique to the digest stream above, since the + // HMac calculation depends on having collected a couple of the header fields CollectingInputStream collectingInputStream = new CollectingInputStream(shaDigestInputStream, true); // make values available from LittleEndian LittleEndianDataInputStream ledis = new LittleEndianDataInputStream(collectingInputStream); @@ -256,10 +246,14 @@ public static KdbxHeader readOuterHeader(InputStream inputStream, KdbxHeader kdb kdbxHeader.setVersion(fullVersion >> 16); // read header fields - getOuterHeaderFields(kdbxHeader, digest, ledis); + getOuterHeaderFields(kdbxHeader, ledis); + // stop digesting + shaDigestInputStream.on(false); // stop collecting the bytes of the header collectingInputStream.setCollecting(false); + + kdbxHeader.setHeaderHash(digest.digest()); kdbxHeader.setHeaderBytes(collectingInputStream.getCollectedBytes()); return kdbxHeader; @@ -272,25 +266,37 @@ public static KdbxHeader readOuterHeader(InputStream inputStream, KdbxHeader kdb * @param input an input source * @throws IOException on error */ - public static void verifyOuterHeader(KdbxHeader kdbxHeader, Credentials credentials, DataInput input) throws IOException { + public static void readOuterHeaderVerification(KdbxHeader kdbxHeader, Credentials credentials, DataInput input) throws IOException { // check the SHA - byte [] sha256 = getBytes(32, input); - if (!Arrays.equals(kdbxHeader.getHeaderHash(), sha256)) { + byte [] receivedSha256 = getBytes(32, input); + if (!Arrays.equals(kdbxHeader.getHeaderHash(), receivedSha256)) { throw new IllegalStateException("Header hash does not match"); } byte[] hmacKey = kdbxHeader.getHmacKey(credentials); - // get the key for the header Hmac (using sequence number -1) // KdbxFile.cs ComputeHeaderHmac byte [] hmacKey64 = Encryption.transformHmacKey(hmacKey, Helpers.toBytes(-1L, ByteOrder.LITTLE_ENDIAN)); - - kdbxHeader.verifyHeaderHmac(hmacKey64, getBytes(32, input)); + Mac mac = Encryption.getHMacSha256Instance(hmacKey64); + byte [] computedHmacSha256 = mac.doFinal(kdbxHeader.getHeaderBytes()); + byte [] receivedHmacSha256 = getBytes(32, input); + if (!Arrays.equals(computedHmacSha256, receivedHmacSha256)) { + throw new IllegalStateException("Header HMAC does not match"); + } } + /** + * Write the hashes required after the header for V4 + * @param kdbxHeader the header whose hashes need to be written + * @param credentials database credentials + * @param dataOutputStream somewhere to write to + */ + private static void writeOuterHeaderVerification(KdbxHeader kdbxHeader, Credentials credentials, DataOutputStream dataOutputStream) throws IOException { + // the SHA digest dataOutputStream.write(kdbxHeader.getHeaderHash()); + // the HMac byte[] hmacKey = kdbxHeader.getHmacKey(credentials); byte[] hmacKey64 = Encryption.transformHmacKey(hmacKey, Helpers.toBytes(-1L, ByteOrder.LITTLE_ENDIAN)); Mac mac = Encryption.getHMacSha256Instance(hmacKey64); @@ -298,7 +304,7 @@ private static void writeOuterHeaderVerification(KdbxHeader kdbxHeader, Credenti dataOutputStream.write(hashedHeaderBytes); } - private static void getOuterHeaderFields(KdbxHeader kdbxHeader, MessageDigest digest, DataInput input) throws IOException { + private static void getOuterHeaderFields(KdbxHeader kdbxHeader, DataInput input) throws IOException { byte headerType; do { headerType = input.readByte(); @@ -352,18 +358,107 @@ private static void getOuterHeaderFields(KdbxHeader kdbxHeader, MessageDigest di break; case HeaderType.KDF_PARAMETERS: - kdbxHeader.setKdfParameters(makeVariantDictionary(length, input)); + kdbxHeader.setKdfParameters(readVariantDictionary(getBytes(length, input))); break; case HeaderType.CUSTOM_DATA: - kdbxHeader.setCustomData(makeVariantDictionary(length, input)); + kdbxHeader.setCustomData(readVariantDictionary(getBytes(length, input))); break; default: throw new IllegalStateException("Unknown File Header"); } } while (headerType != HeaderType.END); + } + + /** + * Write a KdbxHeader to the output stream supplied. The header is updated with the + * message digest of the written stream. + * @param kdbxHeader the header to write and update + * @throws IOException on error + */ + @SuppressWarnings("UnstableApiUsage") + public static void writeKdbxHeader(KdbxHeader kdbxHeader, OutputStream outputStream) throws IOException { + MessageDigest messageDigest = Encryption.getSha256MessageDigestInstance(); + DigestOutputStream digestOutputStream = new DigestOutputStream(outputStream, messageDigest); + CollectingOutputStream collectingOutputStream = new CollectingOutputStream(digestOutputStream); + LittleEndianDataOutputStream ledos = new LittleEndianDataOutputStream(collectingOutputStream); + + // lengths are short in v3 int in v4 + IntConsumer lengthWriter = (i -> { + try { + if (kdbxHeader.getVersion() == 3) { + ledos.writeShort(i); + } else { + ledos.writeInt(i); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + // write the magic number + ledos.writeInt(SIG1); + ledos.writeInt(SIG2); + // write a file version + ledos.writeInt(kdbxHeader.getVersion() == 3?FILE_VERSION_32:FILE_VERSION_4); + + ledos.writeByte(HeaderType.CIPHER_ID); + lengthWriter.accept(16); + byte[] b = new byte[16]; + ByteBuffer bb = ByteBuffer.wrap(b); + bb.putLong(kdbxHeader.getCipherUuid().getMostSignificantBits()); + bb.putLong(8, kdbxHeader.getCipherUuid().getLeastSignificantBits()); + ledos.write(b); + + ledos.writeByte(HeaderType.COMPRESSION_FLAGS); + lengthWriter.accept(4); + ledos.writeInt(kdbxHeader.getCompressionFlags().ordinal()); + + ledos.writeByte(HeaderType.MASTER_SEED); + lengthWriter.accept(kdbxHeader.getMasterSeed().length); + ledos.write(kdbxHeader.getMasterSeed()); + + if (kdbxHeader.getVersion() < 4) { + ledos.writeByte(HeaderType.TRANSFORM_SEED); + lengthWriter.accept(kdbxHeader.getTransformSeed().length); + ledos.write(kdbxHeader.getTransformSeed()); + + ledos.writeByte(HeaderType.TRANSFORM_ROUNDS); + lengthWriter.accept(8); + ledos.writeLong(kdbxHeader.getTransformRounds()); + } + + ledos.writeByte(HeaderType.ENCRYPTION_IV); + lengthWriter.accept(kdbxHeader.getEncryptionIv().length); + ledos.write(kdbxHeader.getEncryptionIv()); + + ledos.writeByte(HeaderType.INNER_RANDOM_STREAM_KEY); + lengthWriter.accept(kdbxHeader.getInnerRandomStreamKey().length); + ledos.write(kdbxHeader.getInnerRandomStreamKey()); + + ledos.writeByte(HeaderType.STREAM_START_BYTES); + lengthWriter.accept(kdbxHeader.getStreamStartBytes().length); + ledos.write(kdbxHeader.getStreamStartBytes()); + + ledos.writeByte(HeaderType.INNER_RANDOM_STREAM_ID); + lengthWriter.accept(4); + ledos.writeInt(kdbxHeader.getProtectedStreamAlgorithm().ordinal()); + + if (kdbxHeader.getVersion() > 3) { + ledos.writeByte(HeaderType.KDF_PARAMETERS); + byte [] vd = serializeVariantDictionary(kdbxHeader.getKdfParameters()); + lengthWriter.accept(vd.length); + ledos.write(vd); + } + + ledos.writeByte(HeaderType.END); + lengthWriter.accept(0); + MessageDigest digest = digestOutputStream.getMessageDigest(); kdbxHeader.setHeaderHash(digest.digest()); + + collectingOutputStream.setCollecting(false); + kdbxHeader.setHeaderBytes(collectingOutputStream.getCollectedBytes()); } /** @@ -429,19 +524,24 @@ public static void writeInnerHeader(KdbxHeader kdbxHeader, OutputStream outputSt output.writeInt(kdbxHeader.getInnerRandomStreamKey().length); output.write(kdbxHeader.getInnerRandomStreamKey()); + for (byte [] binary: kdbxHeader.getBinaries()) { + output.writeByte(InnerHeaderType.BINARY); + output.writeInt(binary.length); + output.write(binary); + } + output.writeByte(InnerHeaderType.END); output.writeInt(0); } /** - * Read a VariantDictionary from the supplied input - * @param input source of data + * Read a VariantDictionary from the supplied input according to KDBX rules + * @param source source of data * @return a VariantDictionary - * @throws IOException on error */ - private static VariantDictionary makeVariantDictionary(int length, DataInput input) throws IOException { + public static VariantDictionary readVariantDictionary(byte [] source) { // read the buffer containing the dictionary, which starts with a 4 byte length - ByteBuffer buf = ByteBuffer.wrap(getBytes(length, input)); + ByteBuffer buf = ByteBuffer.wrap(source); buf.order(ByteOrder.LITTLE_ENDIAN); // version number must be 0x01?? @@ -468,7 +568,12 @@ private static VariantDictionary makeVariantDictionary(int length, DataInput inp return vd; } - private static byte[] serializeVariantDictionary(VariantDictionary v) { + /** + * Serialize a variant dictionary according to KDBX rules + * @param v the dictionary to serialize + * @return a byte array + */ + public static byte[] serializeVariantDictionary(VariantDictionary v) { ByteBuffer buf = ByteBuffer.wrap(new byte[1024]); buf.order(ByteOrder.LITTLE_ENDIAN); buf.mark(); @@ -488,99 +593,18 @@ private static byte[] serializeVariantDictionary(VariantDictionary v) { return result; } - /** - * Write a KdbxHeader to the output stream supplied. The header is updated with the - * message digest of the written stream. - * @param kdbxHeader the header to write and update + * Read two lots of 4 bytes and verify that they satisfy the signature of a kdbx file; + * @param ledis an input stream + * @return true if it looks like this is a kdbx file * @throws IOException on error */ - @SuppressWarnings("UnstableApiUsage") - public static void writeKdbxHeader(KdbxHeader kdbxHeader, OutputStream outputStream) throws IOException { - MessageDigest messageDigest = Encryption.getSha256MessageDigestInstance(); - DigestOutputStream digestOutputStream = new DigestOutputStream(outputStream, messageDigest); - CollectingOutputStream collectingOutputStream = new CollectingOutputStream(digestOutputStream); - LittleEndianDataOutputStream ledos = new LittleEndianDataOutputStream(collectingOutputStream); - - // lengths are short in v3 int in v4 - IntConsumer lengthWriter = (i -> { - try { - if (kdbxHeader.getVersion() == 3) { - ledos.writeShort(i); - } else { - ledos.writeInt(i); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - - // write the magic number - ledos.writeInt(SIG1); - ledos.writeInt(SIG2); - // write a file version - ledos.writeInt(kdbxHeader.getVersion() == 3?FILE_VERSION_32:FILE_VERSION_4); - - ledos.writeByte(HeaderType.CIPHER_ID); - lengthWriter.accept(16); - byte[] b = new byte[16]; - ByteBuffer bb = ByteBuffer.wrap(b); - bb.putLong(kdbxHeader.getCipherUuid().getMostSignificantBits()); - bb.putLong(8, kdbxHeader.getCipherUuid().getLeastSignificantBits()); - ledos.write(b); - - ledos.writeByte(HeaderType.COMPRESSION_FLAGS); - lengthWriter.accept(4); - ledos.writeInt(kdbxHeader.getCompressionFlags().ordinal()); - - ledos.writeByte(HeaderType.MASTER_SEED); - lengthWriter.accept(kdbxHeader.getMasterSeed().length); - ledos.write(kdbxHeader.getMasterSeed()); - - if (kdbxHeader.getVersion() < 4) { - ledos.writeByte(HeaderType.TRANSFORM_SEED); - lengthWriter.accept(kdbxHeader.getTransformSeed().length); - ledos.write(kdbxHeader.getTransformSeed()); - - ledos.writeByte(HeaderType.TRANSFORM_ROUNDS); - lengthWriter.accept(8); - ledos.writeLong(kdbxHeader.getTransformRounds()); - } - - ledos.writeByte(HeaderType.ENCRYPTION_IV); - lengthWriter.accept(kdbxHeader.getEncryptionIv().length); - ledos.write(kdbxHeader.getEncryptionIv()); - - ledos.writeByte(HeaderType.INNER_RANDOM_STREAM_KEY); - lengthWriter.accept(kdbxHeader.getInnerRandomStreamKey().length); - ledos.write(kdbxHeader.getInnerRandomStreamKey()); - - ledos.writeByte(HeaderType.STREAM_START_BYTES); - lengthWriter.accept(kdbxHeader.getStreamStartBytes().length); - ledos.write(kdbxHeader.getStreamStartBytes()); - - ledos.writeByte(HeaderType.INNER_RANDOM_STREAM_ID); - lengthWriter.accept(4); - ledos.writeInt(kdbxHeader.getProtectedStreamAlgorithm().ordinal()); - - if (kdbxHeader.getVersion() > 3) { - ledos.writeByte(HeaderType.KDF_PARAMETERS); - byte [] vd = serializeVariantDictionary(kdbxHeader.getKdfParameters()); - lengthWriter.accept(vd.length); - ledos.write(vd); - } - - ledos.writeByte(HeaderType.END); - lengthWriter.accept(0); - - MessageDigest digest = digestOutputStream.getMessageDigest(); - kdbxHeader.setHeaderHash(digest.digest()); - - collectingOutputStream.setCollecting(false); - kdbxHeader.setHeaderBytes(collectingOutputStream.getCollectedBytes()); + private static boolean verifyMagicNumber(LittleEndianDataInputStream ledis) throws IOException { + int sig1 = ledis.readInt(); + int sig2 = ledis.readInt(); + return sig1 == SIG1 && sig2 == SIG2; } - private static int getInt(int length, DataInput input) throws IOException { if (length != 4) { throw new IllegalStateException("Int required but length was " + length); diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamEncryptor.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamEncryptor.java index 02c68c88..d49c3ead 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamEncryptor.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamEncryptor.java @@ -131,5 +131,4 @@ public byte[] encrypt(byte[] decryptedText) { return output; } } - } diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelperTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelperTest.java index 113e2255..3df9dd37 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelperTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelperTest.java @@ -23,7 +23,7 @@ import static org.junit.Assert.assertEquals; /** - * @author jo + * tests the operation of Hex conversion */ public class HelperTest { @Test @@ -34,12 +34,15 @@ public void testBase64() throws Exception { String convertedString = Helpers.hexStringFromBase64("0tU8XFRUX2TCk5tmmrshuQ=="); System.out.println("Converted string: " + convertedString); assertEquals(hexString, convertedString.toUpperCase()); + UUID uuid = Helpers.uuidFromBase64(inputString); System.out.println("UUID: " + uuid.toString()); assertEquals("d2d53c5c-5454-5f64-c293-9b669abb21b9", uuid.toString()); + String uuidHex = Helpers.hexStringFromUuid(uuid); System.out.println("Hex from UUID: " + uuidHex); assertEquals(hexString, uuidHex.toUpperCase()); + String base64 = Helpers.base64FromUuid(uuid); System.out.println("Base 64: " + base64); assertEquals(inputString, base64); diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java index c4a5a6dc..9a76b62b 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java @@ -16,80 +16,74 @@ package org.linguafranca.pwdb.kdbx; +import com.google.common.io.CharStreams; import org.junit.Test; -import org.linguafranca.pwdb.kdbx.KdbxSerializer; import org.linguafranca.pwdb.Credentials; +import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; /** - * @author jo + * tests reading of kdbx with various combinations of key file and password */ public class KdbxKeyFileTest { - + private static void toConsole(InputStream is) throws IOException { + System.out.println(CharStreams.toString(new InputStreamReader(is, StandardCharsets.UTF_8))); + } + /** + * Test that we can load a key file and get a 32 byte base64 encoded value back + */ @Test public void testLoad() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("KeyFileDatabase.key"); byte[] key = KdbxKeyFile.load(inputStream); assertNotNull(key); - assertEquals(32, key.length); + assertEquals(32, key.length);; } - /* - Test for empty password + /** + * Test that we can read a file with empty password */ @Test public void testEmptyPasswordCreds() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("EmptyPassword.kdbx"); Credentials credentials = new KdbxCreds(new byte[0]); InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); - byte[] buffer = new byte[1024]; - while ( decryptedInputStream.available() > 0) { - int read = decryptedInputStream.read(buffer); - if (read == -1) break; - System.out.write(buffer, 0, read); - } + toConsole(decryptedInputStream); } - /** - Test for empty password with key + Test for empty password with key file */ @Test public void testEmptyPasswordKeyCreds() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("EmptyPasswordWithKey.kdbx"); InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("EmptyPasswordWithKey.key"); + assert inputStreamKeyFile != null; Credentials credentials = new KdbxCreds(new byte[0], inputStreamKeyFile); InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); - byte[] buffer = new byte[1024]; - while ( decryptedInputStream.available() > 0) { - int read = decryptedInputStream.read(buffer); - if (read == -1) break; - System.out.write(buffer, 0, read); - } + toConsole(decryptedInputStream); } /** - Test for no master password + Test for no master password with key */ @Test public void testNoPasswordKeyCreds() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("NoPasswordWithKey.kdbx"); InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("NoPasswordWithKey.key"); + assert inputStreamKeyFile != null; Credentials credentials = new KdbxCreds(inputStreamKeyFile); InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); - byte[] buffer = new byte[1024]; - while ( decryptedInputStream.available() > 0) { - int read = decryptedInputStream.read(buffer); - if (read == -1) break; - System.out.write(buffer, 0, read); - } + toConsole(decryptedInputStream); } - /* + /** Test for empty password */ @Test @@ -97,11 +91,6 @@ public void testEmptyPassword() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("EmptyPassword.kdbx"); Credentials credentials = new KdbxCreds(new byte[0]); InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); - byte[] buffer = new byte[1024]; - while ( decryptedInputStream.available() > 0) { - int read = decryptedInputStream.read(buffer); - if (read == -1) break; - System.out.write(buffer, 0, read); - } + toConsole(decryptedInputStream); } } \ No newline at end of file diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java index e29aeb35..ce1c7905 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java @@ -16,10 +16,8 @@ package org.linguafranca.pwdb.kdbx; -import org.bouncycastle.crypto.params.KDFParameters; import org.junit.Test; import org.linguafranca.pwdb.Credentials; -import org.linguafranca.pwdb.security.VariantDictionary; import java.io.*; import java.util.Scanner; @@ -27,10 +25,13 @@ import static org.junit.Assert.assertEquals; /** - * @author jo + * test operation of kdbx */ public class KdbxSerializerTest { + /** + * Test that we can read a kdbx v3 file and list the XML to console + */ @Test public void testGetPlainTextInputStream() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment.kdbx"); @@ -44,6 +45,10 @@ public void testGetPlainTextInputStream() throws Exception { } } + /** + * Test that we can write a KDBX 3 file containing "Hello World" + * and then read it back to get the same content + */ @Test public void testCypherTextOutputStream() throws Exception { File tempFile = File.createTempFile("test", "test"); @@ -60,6 +65,10 @@ public void testCypherTextOutputStream() throws Exception { assertEquals("Hello World", scanner.nextLine()); } + /** + * Test that we can write a KDBX 4 file containing "Hello World" + * and then read it back to get the same content + */ @Test public void testCypherTextOutputStream2() throws Exception { File tempFile = File.createTempFile("test", "test"); diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java index 3e66bee4..9a2005d9 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java @@ -12,12 +12,12 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import static org.junit.Assert.assertEquals; /** - * @author jo + * test decryption of various kinds */ public class KdbxSerializerTest { @@ -27,15 +27,14 @@ public void getHmacStream() throws IOException { KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); System.out.println("Version " + header.getVersion()); KdbxCreds creds = new KdbxCreds("123".getBytes()); - KdbxSerializer.verifyOuterHeader(header, creds, new LittleEndianDataInputStream(inputStream)); + assert inputStream != null; + //noinspection UnstableApiUsage + KdbxSerializer.readOuterHeaderVerification(header, creds, new LittleEndianDataInputStream(inputStream)); HmacBlockInputStream hmacBlockInputStream = new HmacBlockInputStream(header.getHmacKey(creds), inputStream, true); - byte [] buf = new byte [1024]; - int bytesRead; - while ((bytesRead = hmacBlockInputStream.read(buf)) != -1) { - System.out.println(bytesRead); - } + System.out.println(CharStreams.toString(new InputStreamReader(hmacBlockInputStream, StandardCharsets.UTF_8))); } + // check the correct version @Test public void loadAesheader() throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-AES.kdbx"); @@ -43,6 +42,7 @@ public void loadAesheader() throws IOException { assertEquals(3, header.getVersion()); } + // check correct version v4 @Test public void loadArgonheader() throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); @@ -50,19 +50,25 @@ public void loadArgonheader() throws IOException { assertEquals(4, header.getVersion()); } - + /** + * Check correct content against previously extracted content + */ @Test public void loadChaChaAesHeader() throws IOException { - String expected = CharStreams.toString(new InputStreamReader(KdbxSerializerTest.class.getClassLoader().getResourceAsStream("ChaCha-AES.xml"))); check("ChaCha-AES.xml","V4-ChaCha-AES.kdbx"); } + /** + * Check correct content against previously extracted content + */ @Test public void getDecryptedArgonInputStream() throws IOException { - String expected = CharStreams.toString(new InputStreamReader(KdbxSerializerTest.class.getClassLoader().getResourceAsStream("AES-Argon2.xml"))); check("AES-Argon2.xml","V4-AES-Argon2.kdbx"); } + /** + * Check correct content against previously extracted content + */ @Test public void getDecryptedAesInputStream() throws IOException { check("AES-AES.xml", "V4-AES-AES.kdbx"); @@ -71,7 +77,8 @@ public void getDecryptedAesInputStream() throws IOException { private void check(String compare, String file) throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream(file); InputStream is = KdbxSerializer.createUnencryptedInputStream(new KdbxCreds("123".getBytes()), new KdbxHeader(), inputStream); - String actual = CharStreams.toString(new InputStreamReader(is, Charset.forName("UTF-8"))); + String actual = CharStreams.toString(new InputStreamReader(is, StandardCharsets.UTF_8)); + //noinspection DataFlowIssue String expected = CharStreams.toString(new InputStreamReader(KdbxSerializerTest.class.getClassLoader().getResourceAsStream(compare))); assertEquals(expected, actual); } diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java index 494efd77..62df3993 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java @@ -25,6 +25,7 @@ import static org.junit.Assert.assertEquals; /** + * Verify that the inner random encryption works two-way * @author jo */ public class StreamEncryptionTest { From c190db1ca2f890d842389433a24c775d00e8ce9f Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 5 May 2023 18:19:18 +0100 Subject: [PATCH 083/228] Remove HTTP module, update documentation --- CHANGELOG.md | 10 +- example/pom.xml | 4 + http/pom.xml | 47 ---- http/readme.md | 22 -- .../linguafranca/pwdb/keepasshttp/Crypto.java | 188 --------------- .../pwdb/keepasshttp/DatabaseAdaptor.java | 112 --------- .../keepasshttp/ExampleKeePassHttpServer.java | 43 ---- .../pwdb/keepasshttp/KeePassHttpHandler.java | 88 ------- .../pwdb/keepasshttp/Message.java | 199 --------------- .../pwdb/keepasshttp/Processor.java | 155 ------------ .../pwdb/keepasshttp/PwGenerator.java | 33 --- .../keepasshttp/util/LogginInputStream.java | 72 ------ .../keepasshttp/util/LogginOutputStream.java | 46 ---- .../pwdb/keepasshttp/util/LogginProxy.java | 109 --------- http/src/main/resources/logback.xml | 14 -- .../keepasshttp/MonitoredRequestsTest.java | 109 --------- .../pwdb/keepasshttp/TestRequestResponse.java | 109 --------- http/src/test/java/trace.txt | 228 ------------------ pom.xml | 1 - readme.md | 69 +++--- 20 files changed, 45 insertions(+), 1613 deletions(-) delete mode 100644 http/pom.xml delete mode 100644 http/readme.md delete mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java delete mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java delete mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/ExampleKeePassHttpServer.java delete mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java delete mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java delete mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java delete mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/PwGenerator.java delete mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginInputStream.java delete mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginOutputStream.java delete mode 100644 http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginProxy.java delete mode 100644 http/src/main/resources/logback.xml delete mode 100644 http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java delete mode 100644 http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java delete mode 100644 http/src/test/java/trace.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 85df56be..39054f4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,19 +4,21 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.co ##[2.2-Snapshot] -### Changed +### Added -- Minimum version supported is Java 8 +- support for V4 files -### Added +### Changed -- Readonly support for V4 files +- Minimum version supported is Java 8 - updated documentation of various sorts - kdbx multithreaded fix - update dependencies - spongy castle replaced by bouncy castle - simplexml replaced by simple-xml-safe - update versions throughout +- remove http module +- don't deploy examples to Maven ## [2.1.4] 2018-02-03 diff --git a/example/pom.xml b/example/pom.xml index 1ced0830..0344029f 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -28,6 +28,10 @@ PWDB :: Example Examples of how to use PWDB + + true + + org.linguafranca.pwdb diff --git a/http/pom.xml b/http/pom.xml deleted file mode 100644 index 3c701e37..00000000 --- a/http/pom.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - KeePassJava2-parent - org.linguafranca.pwdb - 2.2-SNAPSHOT - ../pom.xml - - 4.0.0 - - keepasshttp - PWDB :: keepasshttp - - - org.linguafranca.pwdb - KeePassJava2-simple - ${project.version} - - - org.eclipse.jetty - jetty-server - 9.4.51.v20230217 - - - org.slf4j - slf4j-api - - - ch.qos.logback - logback-core - - - ch.qos.logback - logback-classic - - - com.google.code.gson - gson - - - junit - junit - test - - - - \ No newline at end of file diff --git a/http/readme.md b/http/readme.md deleted file mode 100644 index dc4f84ec..00000000 --- a/http/readme.md +++ /dev/null @@ -1,22 +0,0 @@ -#Keepasshttp module - -**_Experimental and Unsafe_** - -This is a Java implementation of the [keepasshttp plugin](https://github.com/pfn/keepasshttp/) -for Windows KeePass. - -It is intended to provide support for the [passifox and chromeipass](https://github.com/pfn/passifox) -browser extensions that allow pasting of credentials from a KeePassDatabse -in a browser context. - -This implementation is an initial one and interworking may not be as good as it could be. - -Also you should note that it does not use https so it is not secure. - -Finally, you should note that any client can connect to the server since the protocol -does not support client credentials. So if you expose this server to the world, -then anyone with a compatible browser plugin can in theory connect -to your database and extract all your secrets. - -**You have been warned sufficently that no representation is made as to -fitness for purpose, safety or anything else.** \ No newline at end of file diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java deleted file mode 100644 index 284b3edc..00000000 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Crypto.java +++ /dev/null @@ -1,188 +0,0 @@ -package org.linguafranca.pwdb.keepasshttp; - -import org.linguafranca.pwdb.kdbx.Helpers; -import org.bouncycastle.crypto.InvalidCipherTextException; -import org.bouncycastle.crypto.engines.AESFastEngine; -import org.bouncycastle.crypto.modes.CBCBlockCipher; -import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; -import org.bouncycastle.crypto.params.KeyParameter; -import org.bouncycastle.crypto.params.ParametersWithIV; - -import java.security.SecureRandom; - -/** - * Cryptography for KeePassHttp emulator - */ -class Crypto { - - private byte[] binaryKey; - - Crypto() { - } - - Crypto(String key) { - this.binaryKey = Helpers.decodeBase64Content(key.getBytes()); - } - - byte[] getKey() { - return binaryKey; - } - - void setKey(byte[] binaryKey) { - this.binaryKey = binaryKey; - } - - enum CMode { - ENCRYPT(true), - DECRYPT(false); - - private final boolean encrypt; - - CMode(boolean encrypt) { - this.encrypt = encrypt; - } - - public boolean getEncrypt() { - return encrypt; - } - } - - /** - * Return true if the Nonce and the Verifier on a message match - * - * @param verifiable a message containing those fields - */ - boolean verify(Message.Verifiable verifiable) { - if (getKey() == null || verifiable.Verifier == null || verifiable.Nonce == null || - verifiable.Verifier.equals("") || verifiable.Nonce.equals("")) { - return false; - } - // The nonce is base64 encoded version of an iv - byte[] iv = Helpers.decodeBase64Content(verifiable.Nonce.getBytes(), false); - // The verifier is the base64 encoded encrypted version of the nonce - String decrypted = decryptFromBase64(verifiable.Verifier, iv); - // the decrypted verifier should be the same as the nonce - return decrypted.equals(verifiable.Nonce); - } - - boolean verify(Message.Request request) { - if (!verify((Message.Verifiable) request)) { - return false; - } - - byte[] iv = Helpers.decodeBase64Content(request.Nonce.getBytes(), false); - // decrypt all the fields - if (request.Login != null) { - request.Login = decryptFromBase64(request.Login, iv); - } - if (request.Password != null) { - request.Password = decryptFromBase64(request.Password, iv); - } - if (request.Url != null) { - request.Url = decryptFromBase64(request.Url, iv); - } - if (request.SubmitUrl != null) { - request.SubmitUrl = decryptFromBase64(request.SubmitUrl, iv); - } - if (request.Uuid != null) { - request.Uuid = decryptFromBase64(request.Uuid, iv); - } - - return true; - } - - /** - * Add a Nonce and a Verifier to a message to make it verifiable - * - * @param response a message to make verifiable - */ - void makeVerifiable(Message.Response response) { - // we don't have a key? we can't do anything - if (getKey() == null) { - return; - } - - // The nonce is base64 encoded version of an iv - byte[] iv = new SecureRandom().generateSeed(16); - response.Nonce = Helpers.encodeBase64Content(iv, false); - // The verifier is the base64 encoded encrypted version of the nonce - response.Verifier = encryptToBase64(response.Nonce, iv); - - // encrypt any entries - if (response.Entries != null) { - for (Message.ResponseEntry entry : response.Entries) { - entry.Login = encryptToBase64(entry.Login, iv); - entry.Uuid = encryptToBase64(entry.Uuid, iv); - entry.Name = encryptToBase64(entry.Name, iv); - entry.Password = encryptToBase64(entry.Password, iv); - } - } - } - - /** - * Get a cipher - * - * @param mode encryption or decryption - * @param iv a 16 byte iv - * @return an initialised Cipher - */ - PaddedBufferedBlockCipher getCipher(CMode mode, byte[] iv) { - PaddedBufferedBlockCipher result = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); - result.init(mode.getEncrypt(), new ParametersWithIV(new KeyParameter(getKey()), iv)); - return result; - } - - /** - * Return an unencrypted non encoded copy of an encrypted base 64 encoded string - * - * @param input cipher text - * @param iv an iv - * @return plain text - */ - String decryptFromBase64(String input, byte[] iv) { - return CryptoTransform(input, true, false, getCipher(CMode.DECRYPT, iv)); - } - - /** - * Return an encrypted base 64 encoded copy of plain text string - * - * @param input plain text - * @param iv an iv - * @return cipher text - */ - String encryptToBase64(String input, byte[] iv) { - return CryptoTransform(input, false, true, getCipher(CMode.ENCRYPT, iv)); - } - - /** - * Encryption and Decryption Helper - * - * @param input the candidate for transformation - * @param base64in true if base 64 encoded - * @param base64out true if we require base 64 out - * @param cipher a Cipher initialised for Encrypt or Decrypt - * @return the transformed result - */ - static String CryptoTransform(String input, boolean base64in, boolean base64out, PaddedBufferedBlockCipher cipher) { - byte[] bytes; - if (base64in) { - bytes = Helpers.decodeBase64Content(input.getBytes(), false); - } else { - bytes = input.getBytes(); - } - - byte[] output = new byte[cipher.getOutputSize(bytes.length)]; - int outputlen = cipher.processBytes(bytes, 0, bytes.length, output, 0); - try { - int len = cipher.doFinal(output, outputlen); - // padded buffer is required on bas64 i.e. encrypted direction - if (base64out) { - return Helpers.encodeBase64Content(output, false); - } - // trim to buffer length - return new String(output, 0, outputlen + len); - } catch (InvalidCipherTextException e) { - throw new IllegalStateException(e); - } - } -} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java deleted file mode 100644 index 397c0e1c..00000000 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/DatabaseAdaptor.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.linguafranca.pwdb.keepasshttp; - -import org.apache.commons.codec.binary.Hex; -import org.linguafranca.pwdb.Credentials; -import org.linguafranca.pwdb.Database; -import org.linguafranca.pwdb.kdbx.Helpers; -import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; -import org.bouncycastle.crypto.digests.SHA1Digest; - -import java.io.*; - -/** - * Adaptor for {@link Database} supporting the requirements of the KeePassHttp protocol. - */ -public interface DatabaseAdaptor { - - /** - * The Id by which this database is known following association - */ - String getId(); - - /** - * Each database has a Hash - */ - String getHash(); - - /** - * A password generator - */ - PwGenerator getPwGenerator(); - - /** - * Where to save, when saving. To be closed by caller. - */ - OutputStream getOutputStream(); - - /** - * Credentials to use, when saving - */ - Credentials getCredentials(); - - /** - * The underlying Database - */ - Database getDatabase(); - - /** - * Default implementation of Adaptor - */ - class Default implements DatabaseAdaptor { - private final Database database; - private final PwGenerator pwGenerator; - private final File databaseFile; - private final Credentials credentials; - - /** - * Constructor for Databse from File - * @param file the file containing the databse - * @param credentials credentials for the databse - * @param pwGenerator a password generator - * @throws Exception if the database can't be constructed - */ - Default(File file, Credentials credentials, PwGenerator pwGenerator) throws Exception { - this.databaseFile = file; - this.pwGenerator = pwGenerator; - this.credentials = credentials; - this.database = SimpleDatabase.load(credentials, new FileInputStream(file)); - } - - @Override - public String getId() { - return database.getName() + " (" + database.getRootGroup().getUuid().toString() + ")"; - } - - // in the C# version this is a hash of the root group UUID and the recycle bin UUID - // we don't have the concept of recycle bin (yet) - @Override - public String getHash() { - byte[] toHash = Helpers.hexStringFromUuid(database.getRootGroup().getUuid()).getBytes(); - SHA1Digest digest = new SHA1Digest(); - byte[] digestBytes = new byte[digest.getDigestSize()]; - digest.update(toHash, 0, toHash.length); - digest.doFinal(digestBytes, 0); - String result = new String(Hex.encodeHex(digestBytes)); - return result.toLowerCase(); - } - - @Override - public Database getDatabase() { - return database; - } - - @Override - public PwGenerator getPwGenerator() { - return pwGenerator; - } - - @Override - public OutputStream getOutputStream() { - try { - return new FileOutputStream(databaseFile); - } catch (FileNotFoundException e) { - throw new IllegalStateException(e); - } - } - - @Override - public Credentials getCredentials() { - return credentials; - } - } -} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/ExampleKeePassHttpServer.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/ExampleKeePassHttpServer.java deleted file mode 100644 index 523ebce3..00000000 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/ExampleKeePassHttpServer.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.linguafranca.pwdb.keepasshttp; - -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.linguafranca.pwdb.kdbx.KdbxCreds; -import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; -import org.linguafranca.pwdb.keepasshttp.PwGenerator.HexPwGenerator; - -import java.io.File; -import java.io.FileOutputStream; -import java.nio.file.Files; -import java.nio.file.Paths; - -/** - * Minimal embedded server - local host origin only but no meaningful security - */ -public class ExampleKeePassHttpServer { - private static final String DEFAULT_DB_FILE = "HttpDatabase.kdbx"; - public static void main(String[] args) throws Exception { - - Server server = new Server(); - - ServerConnector http = new ServerConnector(server); - http.setHost("127.0.0.1"); - http.setPort(19455); - http.setIdleTimeout(300000); - server.addConnector(http); - - KdbxCreds creds = new KdbxCreds("123".getBytes()); - // create a database if we don't have one already - if (Files.notExists(Paths.get(DEFAULT_DB_FILE))) { - SimpleDatabase db = new SimpleDatabase(); - db.setName("HTTP Database"); - db.save(creds,new FileOutputStream(DEFAULT_DB_FILE)); - } - - DatabaseAdaptor adaptor = new DatabaseAdaptor.Default(new File(DEFAULT_DB_FILE), creds, new HexPwGenerator(10)); - server.setHandler(new KeePassHttpHandler(adaptor)); - - server.start(); - server.join(); - } - } diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java deleted file mode 100644 index d2d26fd5..00000000 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/KeePassHttpHandler.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.linguafranca.pwdb.keepasshttp; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.handler.AbstractHandler; -import org.linguafranca.pwdb.kdbx.Helpers; -import org.linguafranca.pwdb.keepasshttp.util.LogginInputStream; -import org.linguafranca.pwdb.keepasshttp.util.LogginOutputStream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.*; - -/** - * Jetty Handler for PassIFox and ChromeIPass clients - emulates KeePassHttp plugin. - */ -public class KeePassHttpHandler extends AbstractHandler { - - private final DatabaseAdaptor adaptor; - private final Processor processor; - private Logger logger = LoggerFactory.getLogger(KeePassHttpHandler.class); - private Gson gson = new GsonBuilder().disableHtmlEscaping().create(); - private Crypto crypto = new Crypto(); - - KeePassHttpHandler(DatabaseAdaptor adaptor) { - this.adaptor = adaptor; - this.processor = new Processor(adaptor); - } - - @Override - public void handle(String s, Request request, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException, ServletException { - - logger.debug("Got a request"); - - InputStream is = new LogginInputStream(request.getInputStream(), logger); - OutputStream outputStream = new LogginOutputStream(httpServletResponse.getOutputStream(), logger); - Writer writer = new BufferedWriter(new OutputStreamWriter(outputStream)); - - Message.Request request1 = gson.fromJson(new BufferedReader(new InputStreamReader(is)),Message.Request.class); - if (request1 == null) { - httpServletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); - request.setHandled(true); - writer.write("That's a 400. JSON not parsed. " + request.getRemoteAddr()); - return; - } - if (request1.RequestType == null) { - httpServletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); - request.setHandled(true); - writer.write("That's a 400. No request type found. " + request.getRemoteAddr()); - return; - } - - Message.Response response = new Message.Response(request1.RequestType, adaptor.getHash()); - - // set the crypto key on associate - if (request1.RequestType.equals(Message.Type.ASSOCIATE)) { - crypto.setKey(Helpers.decodeBase64Content(request1.Key.getBytes(), false)); - } - - // send OK even when it's fail - httpServletResponse.setStatus(HttpServletResponse.SC_OK); - - // normal part of the protocol to fail verification on test-associate - if (!crypto.verify(request1)) { - logger.debug("Request failed verification"); - response.Success = false; - } else { - try { - // processor is responsible for setting success - processor.process(request1, response); - response.Id = adaptor.getId(); - } catch (Exception e) { - httpServletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); - response.Success = false; - response.Error = "Error processing request " + e.getMessage(); - } - } - // presumably errors need to be verifiable? - crypto.makeVerifiable(response); - gson.toJson(response, writer); - writer.flush(); - request.setHandled(true); - } -} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java deleted file mode 100644 index 52ddd7d3..00000000 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Message.java +++ /dev/null @@ -1,199 +0,0 @@ -package org.linguafranca.pwdb.keepasshttp; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; - -/** - * Message definitions for the protocol - *

        - * Note non idiomatic case on field names, this allows correct - * serialization as JSON without annotations - */ -@SuppressWarnings("WeakerAccess") -public class Message { - - /** - * Protocol version - */ - public static final String VERSION = "1.8.4.1"; - - /** - * The valid message types - */ - public static final class Type { - public static final String GET_LOGINS = "get-logins"; - public static final String GET_LOGINS_COUNT = "get-logins-count"; - public static final String GET_ALL_LOGINS = "get-all-logins"; - public static final String SET_LOGIN = "set-login"; - public static final String ASSOCIATE = "associate"; - public static final String TEST_ASSOCIATE = "test-associate"; - public static final String GENERATE_PASSWORD = "generate-password"; - - } - - /** - * Base class for requests and responses, they can be cryptographically verified - */ - public static class Verifiable { - /** - * Nonce value used in conjunction with all encrypted fields, - * randomly generated for each request - */ - public String Nonce; - - /** - * Used to check that the correct key has been chosen - */ - public String Verifier; - } - - /** - * From client to server - */ - public static class Request extends Verifiable { - - public String RequestType; - - /** - * Sort selection by best URL matching for given hosts - */ - public String SortSelection; - - /** - * Trigger unlock of database even if feature is disabled in KPH (because of user interaction to fill-in) - */ - public String TriggerUnlock; - - /** - * Always encrypted, used with set-login, uuid is set - * if modifying an existing login - */ - public String Login; - public String Password; - public String Uuid; - - /** - * Always encrypted, used with get and set-login - */ - public String Url; - - /** - * Always encrypted, used with get-login - */ - public String SubmitUrl; - - /** - * Send the AES key ID with the 'associate' request - */ - public String Key; - - /** - * Always required, an identifier given by the KeePass user - */ - public String Id; - - /** - * Realm value used for filtering results. Always encrypted. - */ - public String Realm; - } - - /** - * From server to client - */ - public static class Response extends Verifiable { - public Response(String request, String hash) { - RequestType = request; - - if (request.equals(Type.GET_LOGINS) || - request.equals(Type.GET_ALL_LOGINS) || - request.equals(Type.GENERATE_PASSWORD)) - Entries = new ArrayList<>(); - else - Entries = null; - - this.Hash = hash; - this.Version = VERSION; - } - - /** - * Mirrors the request type of KeePassRequest - */ - public String RequestType; - - public String Error; - - public boolean Success = false; - - /** - * The user selected String as a result of 'associate', - * always returned on every request - */ - public String Id; - - /** - * response to get-logins-count, number of entries for requested Url - */ - public Integer Count=0; - - /** - * response the current version of KeePassHttp - */ - public String Version = ""; - - /** - * response an unique hash of the database composed of RootGroup UUid and RecycleBin UUid - */ - public String Hash = ""; - - /** - * The resulting entries for a get-login request - */ - public List Entries; - - } - - public static class ResponseEntry { - public ResponseEntry() { - } - - public ResponseEntry(String name, String login, String password, String uuid) { - this(name, login, password, uuid, new ArrayList()); - } - - public ResponseEntry(String name, String login, String password, String uuid, List StringFields) { - this.Login = login; - this.Password = password; - this.Uuid = uuid; - this.Name = name; - this.StringFields = StringFields; - } - - public String Login; - public String Password; - public String Uuid; - public String Name; - public List StringFields = null; - - } - - public static class ResponseStringField { - public ResponseStringField() { - } - - public ResponseStringField(String key, String value) { - Key = key; - Value = value; - } - - public String Key; - public String Value; - } - - static class KeePassHttpEntryConfig { - public HashSet Allow = new HashSet(); - public HashSet Deny = new HashSet(); - public String Realm = null; - } -} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java deleted file mode 100644 index 603501bb..00000000 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/Processor.java +++ /dev/null @@ -1,155 +0,0 @@ -package org.linguafranca.pwdb.keepasshttp; - -import org.linguafranca.pwdb.Database; -import org.linguafranca.pwdb.Entry; -import org.linguafranca.pwdb.keepasshttp.Message.ResponseEntry; - -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.*; - -/** - * Contains message processors for processing messages (doh) - */ -class Processor { - - private interface MessageProcessor { - void process(Message.Request request, Message.Response response); - } - - private final Database database; - private final PwGenerator pwGenerator; - private final DatabaseAdaptor adaptor; - - private Map processors = new HashMap<>(); - - - Processor(DatabaseAdaptor adaptor) { - this.database = adaptor.getDatabase(); - this.pwGenerator = adaptor.getPwGenerator(); - this.adaptor = adaptor; - - processors.put(Message.Type.TEST_ASSOCIATE, new TestAssociate()); - processors.put(Message.Type.ASSOCIATE, new Associate()); - processors.put(Message.Type.GET_LOGINS, new GetLogins()); - processors.put(Message.Type.GET_LOGINS_COUNT, new GetLoginsCount()); - processors.put(Message.Type.GET_ALL_LOGINS, new GetAllLogins()); - processors.put(Message.Type.SET_LOGIN, new SetLogin()); - processors.put(Message.Type.GENERATE_PASSWORD, new GeneratePassword()); - } - - void process(Message.Request request, Message.Response response) { - MessageProcessor mp = processors.get(request.RequestType); - if (mp == null) { - throw new IllegalStateException("Unknown message type " + request.RequestType); - } - mp.process(request, response); - } - - private class GetLogins implements MessageProcessor { - public void process(final Message.Request r, Message.Response resp) { - - @SuppressWarnings("unchecked") - List entries = database.findEntries(new Entry.Matcher() { - @Override - public boolean matches(Entry entry) { - return entry.getUrl().startsWith(r.Url) || r.Url.startsWith(entry.getUrl()); - } - }); - - for (Entry entry : entries) { - resp.Entries.add(new ResponseEntry(entry.getTitle(), entry.getUsername(), entry.getPassword(), entry.getUuid().toString())); - } - resp.Count = resp.Entries.size(); - resp.Success = true; - } - } - - private class GetLoginsCount implements MessageProcessor { - public void process(Message.Request r, Message.Response resp) { - processors.get(Message.Type.GET_LOGINS).process(r, resp); - resp.Entries = null; - } - } - - private class GeneratePassword implements MessageProcessor { - public void process(Message.Request r, Message.Response resp) { - String p = pwGenerator.generate(); - resp.Entries.add(new ResponseEntry("Password", "login", p, UUID.randomUUID().toString())); - resp.Count = resp.Entries.size(); - resp.Success = true; - } - } - - private class GetAllLogins implements MessageProcessor { - public void process(Message.Request r, Message.Response resp) { - @SuppressWarnings("unchecked") - List entries = database.findEntries(new Entry.Matcher() { - @Override - public boolean matches(Entry entry) { - return true; - } - }); - for (Entry entry : entries) { - resp.Entries.add(new ResponseEntry(entry.getTitle(), entry.getUsername(), entry.getPassword(), entry.getUuid().toString())); - } - resp.Count = resp.Entries.size(); - resp.Success = true; - } - } - - private static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - - private class SetLogin implements MessageProcessor { - public void process(final Message.Request r, Message.Response resp) { - Entry entry = null; - if (r.Uuid != null) { - List entries = database.findEntries(new Entry.Matcher() { - @Override - public boolean matches(Entry entry) { - return entry.getUuid().toString().equals(r.Uuid); - } - }); - entry = (Entry) entries.get(0); - } - if (entry == null) { - entry = database.newEntry(); - entry.setTitle("New Entry " + format.format(new Date())); - entry.setNotes("Created automatically"); - } else { - entry.setNotes(entry.getNotes() + "\nUpdated " + format.format(new Date())); - } - entry.setPassword(r.Password); - entry.setUsername(r.Login); - entry.setUrl(r.Url); - entry.setProperty("SubmitUrl", r.SubmitUrl); - //noinspection unchecked - database.getRootGroup().addEntry(entry); - try { - database.save(adaptor.getCredentials(), adaptor.getOutputStream()); - } catch (IOException e) { - throw new IllegalStateException(e); - } - resp.Success = true; - } - } - - private class Associate implements MessageProcessor { - @Override - public void process(Message.Request request, Message.Response response) { - response.Success = true; - } - - } - - private class TestAssociate implements MessageProcessor { - @Override - public void process(Message.Request request, Message.Response response) { - response.Success = false; - if (request.Id != null) { - response.Success = request.Id.equals(adaptor.getId()); - } - } - - } -} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/PwGenerator.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/PwGenerator.java deleted file mode 100644 index 3f00d39a..00000000 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/PwGenerator.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.linguafranca.pwdb.keepasshttp; - -import java.security.SecureRandom; -import java.util.Random; - -/** - * Interface for an injectable password generator - */ -public interface PwGenerator { - String generate(); - /** - * Generator for simple hex password - */ - class HexPwGenerator implements PwGenerator { - private final int length; - - HexPwGenerator (int length) { - this.length = length; - } - - @Override - public String generate() { - String[] symbols = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"}; - Random random = new SecureRandom(); - StringBuilder sb = new StringBuilder(length); - for (int i = 0; i < length; i++) { - int indexRandom = random.nextInt( symbols.length ); - sb.append( symbols[indexRandom] ); - } - return sb.toString(); - } - } -} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginInputStream.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginInputStream.java deleted file mode 100644 index a5d39b4d..00000000 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginInputStream.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.linguafranca.pwdb.keepasshttp.util; - -import org.slf4j.Logger; - -import java.io.IOException; -import java.io.InputStream; - -/** - * @author jo - */ -public class LogginInputStream extends InputStream { - - InputStream is; - Logger logger; - - public LogginInputStream(InputStream is, Logger logger) { - this.is = is; - this.logger = logger; - } - - @Override - public int read() throws IOException { - int i = is.read(); - return is.read(); - } - - @Override - public int read(byte[] b) throws IOException { - return is.read(b); - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - - int i = is.read(b, off, len); - if (i>=0) { - String s = new String(b, off, i); - logger.info("--> " + s); - } - return i; - } - - @Override - public long skip(long n) throws IOException { - return is.skip(n); - } - - @Override - public int available() throws IOException { - return is.available(); - } - - @Override - public void close() throws IOException { - is.close(); - } - - @Override - public void mark(int readlimit) { - is.mark(readlimit); - } - - @Override - public void reset() throws IOException { - is.reset(); - } - - @Override - public boolean markSupported() { - return is.markSupported(); - } -} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginOutputStream.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginOutputStream.java deleted file mode 100644 index 2bec2c60..00000000 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginOutputStream.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.linguafranca.pwdb.keepasshttp.util; - -import org.slf4j.Logger; - -import java.io.IOException; -import java.io.OutputStream; - -/** - * @author jo - */ -public class LogginOutputStream extends OutputStream { - - Logger logger; - OutputStream outputStream; - - public LogginOutputStream(OutputStream outputStream, Logger logger) { - this.logger = logger; - this.outputStream = outputStream; - } - - @Override - public void write(int b) throws IOException { - outputStream.write(b); - } - - @Override - public void write(byte[] b) throws IOException { - outputStream.write(b); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - logger.info("<-- " + new String(b, off, len)); - outputStream.write(b, off, len); - } - - @Override - public void flush() throws IOException { - outputStream.flush(); - } - - @Override - public void close() throws IOException { - outputStream.close(); - } -} diff --git a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginProxy.java b/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginProxy.java deleted file mode 100644 index c04a4469..00000000 --- a/http/src/main/java/org/linguafranca/pwdb/keepasshttp/util/LogginProxy.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.linguafranca.pwdb.keepasshttp.util; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.ServerSocket; -import java.net.Socket; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * @author jo - */ -public class LogginProxy { - private static final AtomicInteger count = new AtomicInteger(0); - - public static Logger logger = LoggerFactory.getLogger("proxy"); - - public LogginProxy(){ - - } - - public static void main(String[] args) throws IOException { - go(); - } - - public static void go() throws IOException { - ServerSocket serverSocket = new ServerSocket(19455); - do { - logger.info("Awaiting connection"); - Socket clientSocket = serverSocket.accept(); - Executors.newSingleThreadExecutor().submit(new Service(clientSocket)); - }while (true); - - } - - private static class Service implements Callable { - private final Socket clientSocket; - - private Service(Socket socket) { - this.clientSocket = socket; - } - - @Override - public Boolean call() throws Exception { - final String threadName = "Connection " + count.getAndIncrement(); - Thread.currentThread().setName(threadName); - final InputStream serverInputStream = clientSocket.getInputStream(); - final OutputStream serverOutputStream = clientSocket.getOutputStream(); - - Socket forwardSocket = new Socket("192.168.1.131", 19456); - final InputStream clientInputStream = forwardSocket.getInputStream(); - final OutputStream clientOutputStream = forwardSocket.getOutputStream(); - - Callable upstream = new Callable() { - @Override - public Boolean call() throws Exception { - Thread.currentThread().setName(threadName + " ->"); - logger.info("upstream starting"); - byte[] b = new byte[1024]; - int l; - while ((l=serverInputStream.read(b)) > -1) { - clientOutputStream.write(b,0,l); - clientOutputStream.flush(); - logger.info(new String(b, 0, l)); - } - logger.info("upstream finished"); - return true; - } - }; - - Callable downstream = new Callable() { - @Override - public Boolean call() throws Exception { - Thread.currentThread().setName(threadName + " <-"); - logger.info("downstream starting"); - byte[] b = new byte[1024]; - int l; - while ((l = clientInputStream.read(b)) > -1) { - serverOutputStream.write(b,0,l); - serverOutputStream.flush(); - logger.info(new String(b, 0, l)); - } - logger.info("downstream finished"); - return true; - } - }; - Future upstreamFuture = Executors.newSingleThreadExecutor().submit(upstream); - Future downStreamFuture = Executors.newSingleThreadExecutor().submit(downstream); - try { - upstreamFuture.get(); - //forwardSocket.shutdownOutput(); - downStreamFuture.get(); - forwardSocket.shutdownOutput(); - clientSocket.shutdownOutput(); - logger.info("Connection finished"); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - return true; - } - } -} diff --git a/http/src/main/resources/logback.xml b/http/src/main/resources/logback.xml deleted file mode 100644 index afaebf8e..00000000 --- a/http/src/main/resources/logback.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - \ No newline at end of file diff --git a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java deleted file mode 100644 index 42e008e0..00000000 --- a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/MonitoredRequestsTest.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.linguafranca.pwdb.keepasshttp; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.junit.Test; -import org.linguafranca.pwdb.kdbx.Helpers; -import org.linguafranca.pwdb.kdbx.KdbxCreds; -import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; - -import static org.junit.Assert.fail; - -/** - * @author jo - */ -public class MonitoredRequestsTest { - private static String associateRequest = "{\"RequestType\":\"associate\",\"Key\":\"NTxz7Rej/a4H9OFXBmXxMp0BjIfqhy5QEYjLnqhWdkA=\",\"Nonce\":\"UJUjvviWfVDK7c6KzZNUkw==\",\"Verifier\":\"f3DwAdI7rcWAH0DzGlCMTjLtH2q2+eBLNgRuojQWLS8=\"}"; - private static String associateResponse = "{\"RequestType\":\"associate\",\"Success\":true,\"Id\":\"NEW coNECTION\",\"Count\":0,\"Version\":\"1.8.4.1\",\"Hash\":\"eefc4faf792a59d034da69c69f3643ef34ba7d8d\",\"Nonce\":\"/cdtpNsn3sjiwVtrwYmx3g==\",\"Verifier\":\"5onGXGq18yT4BM3QJnqUxXMKJ7/4fnRvjZ6R0OO0RSE=\"}"; - - private static String testAssociateRequestWithId = "{\"RequestType\":\"test-associate\",\"TriggerUnlock\":false,\"Id\":\"NEW coNECTION\",\"Nonce\":\"gWgoWCbp6/9YsuvVH3hSPw==\",\"Verifier\":\"NQ0d51+hW4Y7NBB0Ns6QKPg0BSWnHwHzleMKpZsdO54=\"}"; - private static String testAssociateResponseWithId = "{\"RequestType\":\"test-associate\",\"Success\":true,\"Id\":\"NEW coNECTION\",\"Count\":0,\"Version\":\"1.8.4.1\",\"Hash\":\"eefc4faf792a59d034da69c69f3643ef34ba7d8d\",\"Nonce\":\"HMtkcQjmeRxxm8/GLcPsAQ==\",\"Verifier\":\"t+wp+yIoG497STtDUf7st9yGSLStHglihAO+GMFHnzg=\"}"; - - private static String getLoginsRequest = "{\"RequestType\":\"get-logins\",\"SortSelection\":\"true\",\"TriggerUnlock\":\"false\",\"Id\":\"NEW coNECTION\",\"Nonce\":\"79y7EyDVndGn3B5iFLp4Kw==\",\"Verifier\":\"LIY9XDHscI+xgrzIVCFnqsW9yigiNzWbJ1ZpOIBKGVs=\",\"Url\":\"eZpW5dUli05H5l1+MuwiUNjXZyNemZDmQOFjgkz8uvg=\",\"SubmitUrl\":\"eZpW5dUli05H5l1+MuwiUO2/H/Faw6yvjvgrCElBRSSbGRJ3a4hXFdq0ws75fK26XT9SQQXqwOvCiTiK5Ybnxg==\"}"; - private static String getLoginsResponse = "{\"RequestType\":\"get-logins\",\"Success\":true,\"Id\":\"NEW coNECTION\",\"Count\":1,\"Version\":\"1.8.4.1\",\"Hash\":\"eefc4faf792a59d034da69c69f3643ef34ba7d8d\",\"Nonce\":\"hQ2JSllJLrVWv1LcxN8+rQ==\",\"Verifier\":\"TT2014GvCXk+bruePydwqiGFeupP+xv7F7UnC5lc7vo=\",\"Entries\":[{\"Login\":\"n1qi36EQkTQhzQiT7TFonA==\",\"Password\":\"gvgmfbelEa3QxvzRsEG0Hg==\",\"Uuid\":\"w5MJa4za3DLh+2CmbG9Yl5kr5Gba0wnptN24kZPsWHPA8hPx6Pb1XYxiseh5gARY\",\"Name\":\"xpCqAwBjmUOqVyI6dfJqGA==\"}]}"; - - Gson gson = new GsonBuilder().disableHtmlEscaping().create(); - - File tempFile; - - public MonitoredRequestsTest() { - KdbxCreds creds = new KdbxCreds("123".getBytes()); - - SimpleDatabase db = new SimpleDatabase(); - db.setName("Test Database"); - try { - tempFile = File.createTempFile("pwdb", "tmp"); - db.save(creds, new FileOutputStream(tempFile)); - } catch (Exception e) { - fail(); - } - } - - @Test - public void sessionTest() throws Exception { - Processor processor = new Processor(new DatabaseAdaptor.Default(tempFile, - new KdbxCreds("123".getBytes()), - new PwGenerator() { - @Override - public String generate() { - return "123"; - } - })); - - Message.Request request = gson.fromJson(associateRequest, Message.Request.class); - Crypto crypto = new Crypto(request.Key); - Message.Response actualResponse = gson.fromJson(associateResponse, Message.Response.class); - - Message.Response computedResponse = new Message.Response(request.RequestType, "eefc4faf792a59d034da69c69f3643ef34ba7d8d"); - processor.process(request, computedResponse); - computedResponse.Id = "NEW coNECTION"; - - crypto.makeVerifiable(computedResponse); - - System.out.println(gson.toJson(computedResponse)); - crypto.verify(computedResponse); - System.out.println(gson.toJson(actualResponse)); - crypto.verify(actualResponse); - - - request = gson.fromJson(testAssociateRequestWithId, Message.Request.class); - actualResponse = gson.fromJson(testAssociateResponseWithId, Message.Response.class); - - computedResponse = new Message.Response(request.RequestType, "eefc4faf792a59d034da69c69f3643ef34ba7d8d"); - processor.process(request, computedResponse); - crypto.makeVerifiable(computedResponse); - computedResponse.Id = "NEW coNECTION"; - - System.out.println(gson.toJson(computedResponse)); - crypto.verify(computedResponse); - System.out.println(gson.toJson(actualResponse)); - crypto.verify(actualResponse); - - - request = gson.fromJson(getLoginsRequest, Message.Request.class); - actualResponse = gson.fromJson(getLoginsResponse, Message.Response.class); - - computedResponse = new Message.Response(request.RequestType, "eefc4faf792a59d034da69c69f3643ef34ba7d8d"); - processor.process(request, computedResponse); - crypto.makeVerifiable(computedResponse); - computedResponse.Id = "NEW coNECTION"; - - System.out.println(gson.toJson(computedResponse)); - crypto.verify(computedResponse); - System.out.println(gson.toJson(actualResponse)); - crypto.verify(actualResponse); - byte[] iv = Helpers.decodeBase64Content(actualResponse.Nonce.getBytes(), false); - for (Message.ResponseEntry entry : actualResponse.Entries) { - System.out.println(crypto.decryptFromBase64(entry.Login, iv)); - System.out.println(crypto.decryptFromBase64(entry.Name, iv)); - System.out.println(crypto.decryptFromBase64(entry.Password, iv)); - System.out.println(crypto.decryptFromBase64(entry.Uuid, iv)); - } - - - } -} diff --git a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java b/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java deleted file mode 100644 index 30775a86..00000000 --- a/http/src/test/java/org/linguafranca/pwdb/keepasshttp/TestRequestResponse.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.linguafranca.pwdb.keepasshttp; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.junit.Test; -import org.linguafranca.pwdb.kdbx.Helpers; -import org.linguafranca.pwdb.kdbx.KdbxCreds; -import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; -import org.linguafranca.pwdb.keepasshttp.Crypto; -import org.linguafranca.pwdb.keepasshttp.Processor; -import org.linguafranca.pwdb.keepasshttp.Message; -import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.security.SecureRandom; -import java.util.ArrayList; - -import static org.junit.Assert.*; - -/** - * @author jo - */ -public class TestRequestResponse { - private static String requestString = "{\"RequestType\":\"associate\",\"Key\":\"WQISYVajkviMNaSBIaUTgZbUaJD1RujU6eoh3B89+lc=\",\"Nonce\":\"V2bJ4VFRrbH+oUd0W8swjQ==\",\"Verifier\":\"oegZmDzOLFVv8jAYxfnOqBk+0KiasSr6J/8v9xbUIkE=\"}"; - private static String getLoginsAssociate ="{\"RequestType\":\"associate\",\"Key\":\"W4z49b+uO2vLGzdqBzfKSY/ynmfLRcxfBYmk99OIT3k=\",\"Nonce\":\"9N3pLnqsXgyh3H7wGXrMQQ==\",\"Verifier\":\"b1z9Rhd8pYSUEczdPx4wWyQTtB9oQFjNzx5EfOfqdNw=\"}"; - private static String getLogins ="{\"RequestType\":\"get-logins\",\"SortSelection\":\"true\",\"TriggerUnlock\":\"false\",\"Id\":\"1234\",\"Nonce\":\"v/93UGpWJYaglsrWTFsq6A==\",\"Verifier\":\"kltoi/9kt2rrv5SReMDYN7zX1XDeBdCPvU78m0qWqVQ=\",\"Url\":\"RNTCyUHMs3smRf99uQx0j/Y0T2dCDAEmYJyx1Gt9zhs=\",\"SubmitUrl\":\"RNTCyUHMs3smRf99uQx0jxIm9b/18UmkIR70L37lxckUpUNjh8Oxre5P6yc74d93OHG3f3OrYEpsItig+IXOAQ==\"}"; - - - private static String associate="{\"RequestType\":\"associate\",\"Key\":\"S7rQME6+2flB7ZJ01sH3aJYyq0KIDCpHtvu1vZspgXs=\",\"Nonce\":\"Z3fBN6zrgceHvnAb/OnwKg==\",\"Verifier\":\"ZQ+KrgSjICAMzocHizFhrJ/8/EyCnBcdypICtIrW1IU=\"}"; - private static String response = "{\"RequestType\":\"associate\",\"Error\":\"\",\"Success\":true,\"Id\":\"402881E9-58B6-5A30-0158-B65A30B20000\",\"Count\":null,\"Version\":\"1.8.4.1\",\"Hash\":\"d51377aeb06c1707f56c0b323662ddf41c777b0c\",\"Entries\":null,\"Nonce\":\"+XFkT0BRzkj/zW7N6W0g+w==\",\"Verifier\":\"0geZx858HFVFYl8b1gV4bAySuBasGnIbh4FdOv8Y/SM=\"}"; - private static String reTest = "{\"RequestType\":\"test-associate\",\"TriggerUnlock\":false,\"Id\":\"402881E9-58B6-5A30-0158-B65A30B20000\",\"Nonce\":\"+/UY+Mg4I8mSJweoNg1x+Q==\",\"Verifier\":\"GnhpnRBsCkT7LGcjZdBtMpgRWo3XFqf8Kwyxj4P3jbI=\"}"; - private static String reResponse = "{\"RequestType\":\"test-associate\",\"Error\":\"\",\"Success\":true,\"Id\":null,\"Count\":null,\"Version\":\"1.8.4.1\",\"Hash\":\"d51377aeb06c1707f56c0b323662ddf41c777b0c\",\"Entries\":null,\"Nonce\":\"QaYPVoWvF+k31MQGuyLEEA==\",\"Verifier\":\"kOReuG3a0l+do6/8xC58QjNVqgMrAADvt7agyczuPtk=\"}"; - private Gson gson = new GsonBuilder().disableHtmlEscaping().create(); - - File tempFile; - public TestRequestResponse() { - KdbxCreds creds = new KdbxCreds("123".getBytes()); - - SimpleDatabase db = new SimpleDatabase(); - db.setName("Test Database"); - try { - tempFile = File.createTempFile("pwdb", "tmp"); - db.save(creds, new FileOutputStream(tempFile)); - }catch (Exception e) { - fail(); - } - - } - @Test - public void testVerifyAssociate() { - Message.Request r = gson.fromJson(associate, Message.Request.class); - Crypto crypto = new Crypto(r.Key); - assertArrayEquals(r.Key.getBytes(), Helpers.encodeBase64Content(crypto.getKey()).getBytes()); - - byte[] iv = new SecureRandom().generateSeed(16); - String secret = crypto.encryptToBase64("Secret", iv); - assertEquals("Secret", crypto.decryptFromBase64(secret, iv)); - - assertTrue(crypto.verify(r)); - assertTrue(crypto.verify(gson.fromJson(response, Message.Response.class))); - assertTrue(crypto.verify(gson.fromJson(reTest, Message.Request.class))); - assertTrue(crypto.verify(gson.fromJson(reResponse, Message.Response.class))); - } - - - @Test - public void testVerifyAssociateRequestResponse() { - Message.Request r = gson.fromJson(requestString, Message.Request.class); - Crypto crypto = new Crypto(r.Key); - assertTrue(crypto.verify(r)); - - Message.Response response = new Message.Response(r.RequestType,""); - crypto.makeVerifiable(response); - - assertTrue(crypto.verify(response)); - - } - @Test - public void testGetLogins() throws Exception { - Message.Request r = gson.fromJson(getLoginsAssociate, Message.Request.class); - Crypto crypto = new Crypto(r.Key); - crypto.verify(r); - - Message.Request l = gson.fromJson(getLogins, Message.Request.class); - assertTrue(crypto.verify(l)); - assertEquals("https://www.facebook.com", l.Url); - assertEquals("https://www.facebook.com/login.php?login_attempt=1&lwv=110", l.SubmitUrl); - - Message.Response response = new Message.Response(l.RequestType, new DatabaseAdaptor.Default(tempFile, - new KdbxCreds("123".getBytes()), - new PwGenerator() { - @Override - public String generate() { - return "123"; - } - }).getHash()); - response.Success=true; - response.Count=1; - response.Entries.add(new Message.ResponseEntry("a", "b", "c","uuid", new ArrayList())); - crypto.makeVerifiable(response); - - assertTrue(crypto.verify(response)); - } - - -} diff --git a/http/src/test/java/trace.txt b/http/src/test/java/trace.txt deleted file mode 100644 index 3833b0ef..00000000 --- a/http/src/test/java/trace.txt +++ /dev/null @@ -1,228 +0,0 @@ -18:26:34.769 [main] INFO proxy - Awaiting connection -18:26:51.472 [Connection 0 ->] INFO proxy - upstream starting -18:26:51.472 [Connection 0 <-] INFO proxy - downstream starting - - - -18:26:51.473 [Connection 0 ->] INFO proxy - POST / HTTP/1.1 -Host: localhost:19455 -Connection: keep-alive -Content-Length: 54 -User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36 -Origin: chrome-extension://ompiailgknfdndiefoaoiligalphfdae -Content-Type: application/json -Accept: */* -Accept-Encoding: gzip, deflate, br -Accept-Language: en-US,en;q=0.8 - -{"RequestType":"test-associate","TriggerUnlock":false} - - - -18:26:51.541 [Connection 0 <-] INFO proxy - HTTP/1.1 200 OK -Content-Length: 128 -Content-Type: application/json -Server: Microsoft-HTTPAPI/2.0 -Date: Mon, 31 Oct 2016 18:26:51 GMT - -{"RequestType":"test-associate","Success":false,"Count":0,"Version":"1.8.4.1","Hash":"eefc4faf792a59d034da69c69f3643ef34ba7d8d"} - - - -18:26:57.523 [Connection 0 ->] INFO proxy - POST / HTTP/1.1 -Host: localhost:19455 -Connection: keep-alive -Content-Length: 54 -User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36 -Origin: chrome-extension://ompiailgknfdndiefoaoiligalphfdae -Content-Type: application/json -Accept: */* -Accept-Encoding: gzip, deflate, br -Accept-Language: en-US,en;q=0.8 - -{"RequestType":"test-associate","TriggerUnlock":false} - - - -18:26:57.589 [Connection 0 <-] INFO proxy - HTTP/1.1 200 OK -Content-Length: 128 -Content-Type: application/json -Server: Microsoft-HTTPAPI/2.0 -Date: Mon, 31 Oct 2016 18:26:57 GMT - -{"RequestType":"test-associate","Success":false,"Count":0,"Version":"1.8.4.1","Hash":"eefc4faf792a59d034da69c69f3643ef34ba7d8d"} - - - -18:26:57.594 [Connection 0 ->] INFO proxy - POST / HTTP/1.1 -Host: localhost:19455 -Connection: keep-alive -Content-Length: 173 -User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36 -Origin: chrome-extension://ompiailgknfdndiefoaoiligalphfdae -Content-Type: application/json -Accept: */* -Accept-Encoding: gzip, deflate, br -Accept-Language: en-US,en;q=0.8 - -{"RequestType":"associate","Key":"NTxz7Rej/a4H9OFXBmXxMp0BjIfqhy5QEYjLnqhWdkA=","Nonce":"UJUjvviWfVDK7c6KzZNUkw==","Verifier":"f3DwAdI7rcWAH0DzGlCMTjLtH2q2+eBLNgRuojQWLS8="} - - - -18:27:28.276 [Connection 0 <-] INFO proxy - HTTP/1.1 200 OK -Content-Length: 236 -Content-Type: application/json -Server: Microsoft-HTTPAPI/2.0 -Date: Mon, 31 Oct 2016 18:27:28 GMT - -{"RequestType":"associate","Success":true,"Id":"NEW coNECTION","Count":0,"Version":"1.8.4.1","Hash":"eefc4faf792a59d034da69c69f3643ef34ba7d8d","Nonce":"/cdtpNsn3sjiwVtrwYmx3g==","Verifier":"5onGXGq18yT4BM3QJnqUxXMKJ7/4fnRvjZ6R0OO0RSE="} - - - -18:27:49.553 [Connection 0 ->] INFO proxy - POST / HTTP/1.1 -Host: localhost:19455 -Connection: keep-alive -Content-Length: 54 -User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36 -Origin: chrome-extension://ompiailgknfdndiefoaoiligalphfdae -Content-Type: application/json -Accept: */* -Accept-Encoding: gzip, deflate, br -Accept-Language: en-US,en;q=0.8 - -{"RequestType":"test-associate","TriggerUnlock":false} - - - -18:27:49.619 [Connection 0 <-] INFO proxy - HTTP/1.1 200 OK -Content-Length: 128 -Content-Type: application/json -Server: Microsoft-HTTPAPI/2.0 -Date: Mon, 31 Oct 2016 18:27:49 GMT - -{"RequestType":"test-associate","Success":false,"Count":0,"Version":"1.8.4.1","Hash":"eefc4faf792a59d034da69c69f3643ef34ba7d8d"} - - - -18:29:38.604 [Connection 0 ->] INFO proxy - POST / HTTP/1.1 -Host: localhost:19455 -Connection: keep-alive -Content-Length: 54 -User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36 -Origin: chrome-extension://ompiailgknfdndiefoaoiligalphfdae -Content-Type: application/json -Accept: */* -Accept-Encoding: gzip, deflate, br -Accept-Language: en-US,en;q=0.8 - -{"RequestType":"test-associate","TriggerUnlock":false} - - - -18:29:38.663 [Connection 0 <-] INFO proxy - HTTP/1.1 200 OK -Content-Length: 128 -Content-Type: application/json -Server: Microsoft-HTTPAPI/2.0 -Date: Mon, 31 Oct 2016 18:29:38 GMT - -{"RequestType":"test-associate","Success":false,"Count":0,"Version":"1.8.4.1","Hash":"eefc4faf792a59d034da69c69f3643ef34ba7d8d"} - - - -18:29:38.670 [Connection 0 ->] INFO proxy - POST / HTTP/1.1 -Host: localhost:19455 -Connection: keep-alive -Content-Length: 345 -User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36 -Origin: chrome-extension://ompiailgknfdndiefoaoiligalphfdae -Content-Type: application/json -Accept: */* -Accept-Encoding: gzip, deflate, br -Accept-Language: en-US,en;q=0.8 - -{"RequestType":"get-logins","SortSelection":"true","TriggerUnlock":"false","Id":"NEW coNECTION","Nonce":"0zKxhTxwUiGfPYtu0WPrhQ==","Verifier":"7hOpUKLJUyFeIdZiBBUFTwV9JzrdxMSELdeaoXt2ias=","Url":"thIyzORoaXem7BvHrrG+BCEOZ9B2hrWFbyTUEE+Ua6o=","SubmitUrl":"thIyzORoaXem7BvHrrG+BIyLvjut2ACp3PmMgnvDzI3vh+pEe4XyKg9R+Oum1q1b62z+9mrXwpOKtnR/g9ogTQ=="} - - - -18:29:38.758 [Connection 0 <-] INFO proxy - HTTP/1.1 200 OK -Content-Length: 250 -Content-Type: application/json -Server: Microsoft-HTTPAPI/2.0 -Date: Mon, 31 Oct 2016 18:29:38 GMT - -{"RequestType":"get-logins","Success":true,"Id":"NEW coNECTION","Count":0,"Version":"1.8.4.1","Hash":"eefc4faf792a59d034da69c69f3643ef34ba7d8d","Nonce":"NARnSmq7mlnaqNuZLpE3WQ==","Verifier":"Sq4ayYZIz86EO6MSjflPfsP7g7jWg85moHnwqA8V0do=","Entries":[]} - - - -18:34:31.067 [Connection 0 ->] INFO proxy - POST / HTTP/1.1 -Host: localhost:19455 -Connection: keep-alive -Content-Length: 54 -User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36 -Origin: chrome-extension://ompiailgknfdndiefoaoiligalphfdae -Content-Type: application/json -Accept: */* -Accept-Encoding: gzip, deflate, br -Accept-Language: en-US,en;q=0.8 - -{"RequestType":"test-associate","TriggerUnlock":false} - - - -18:34:31.131 [Connection 0 <-] INFO proxy - HTTP/1.1 200 OK -Content-Length: 128 -Content-Type: application/json -Server: Microsoft-HTTPAPI/2.0 -Date: Mon, 31 Oct 2016 18:34:31 GMT - -{"RequestType":"test-associate","Success":false,"Count":0,"Version":"1.8.4.1","Hash":"eefc4faf792a59d034da69c69f3643ef34ba7d8d"} - - - -18:34:31.135 [Connection 0 ->] INFO proxy - POST / HTTP/1.1 -Host: localhost:19455 -Connection: keep-alive -Content-Length: 168 -User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36 -Origin: chrome-extension://ompiailgknfdndiefoaoiligalphfdae -Content-Type: application/json -Accept: */* -Accept-Encoding: gzip, deflate, br -Accept-Language: en-US,en;q=0.8 - -{"RequestType":"test-associate","TriggerUnlock":false,"Id":"NEW coNECTION","Nonce":"gWgoWCbp6/9YsuvVH3hSPw==","Verifier":"NQ0d51+hW4Y7NBB0Ns6QKPg0BSWnHwHzleMKpZsdO54="} - - - -18:34:31.193 [Connection 0 <-] INFO proxy - HTTP/1.1 200 OK -Content-Length: 241 -Content-Type: application/json -Server: Microsoft-HTTPAPI/2.0 -Date: Mon, 31 Oct 2016 18:34:31 GMT - -{"RequestType":"test-associate","Success":true,"Id":"NEW coNECTION","Count":0,"Version":"1.8.4.1","Hash":"eefc4faf792a59d034da69c69f3643ef34ba7d8d","Nonce":"HMtkcQjmeRxxm8/GLcPsAQ==","Verifier":"t+wp+yIoG497STtDUf7st9yGSLStHglihAO+GMFHnzg="} - - - -18:34:31.207 [Connection 0 ->] INFO proxy - POST / HTTP/1.1 -Host: localhost:19455 -Connection: keep-alive -Content-Length: 345 -User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36 -Origin: chrome-extension://ompiailgknfdndiefoaoiligalphfdae -Content-Type: application/json -Accept: */* -Accept-Encoding: gzip, deflate, br -Accept-Language: en-US,en;q=0.8 - -{"RequestType":"get-logins","SortSelection":"true","TriggerUnlock":"false","Id":"NEW coNECTION","Nonce":"79y7EyDVndGn3B5iFLp4Kw==","Verifier":"LIY9XDHscI+xgrzIVCFnqsW9yigiNzWbJ1ZpOIBKGVs=","Url":"eZpW5dUli05H5l1+MuwiUNjXZyNemZDmQOFjgkz8uvg=","SubmitUrl":"eZpW5dUli05H5l1+MuwiUO2/H/Faw6yvjvgrCElBRSSbGRJ3a4hXFdq0ws75fK26XT9SQQXqwOvCiTiK5Ybnxg=="} - - - -18:34:52.694 [Connection 0 <-] INFO proxy - HTTP/1.1 200 OK -Content-Length: 432 -Content-Type: application/json -Server: Microsoft-HTTPAPI/2.0 -Date: Mon, 31 Oct 2016 18:34:52 GMT - -{"RequestType":"get-logins","Success":true,"Id":"NEW coNECTION","Count":1,"Version":"1.8.4.1","Hash":"eefc4faf792a59d034da69c69f3643ef34ba7d8d","Nonce":"hQ2JSllJLrVWv1LcxN8+rQ==","Verifier":"TT2014GvCXk+bruePydwqiGFeupP+xv7F7UnC5lc7vo=","Entries":[{"Login":"n1qi36EQkTQhzQiT7TFonA==","Password":"gvgmfbelEa3QxvzRsEG0Hg==","Uuid":"w5MJa4za3DLh+2CmbG9Yl5kr5Gba0wnptN24kZPsWHPA8hPx6Pb1XYxiseh5gARY","Name":"xpCqAwBjmUOqVyI6dfJqGA=="}]} diff --git a/pom.xml b/pom.xml index 7598a3d4..0116f611 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,6 @@ jaxb all example - http pom diff --git a/readme.md b/readme.md index f4eafdf5..3e56bc5f 100644 --- a/readme.md +++ b/readme.md @@ -1,15 +1,13 @@ # KeePassJava2 [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent) -Master: [![Build Status](https://travis-ci.org/jorabin/KeePassJava2.svg?branch=master)](https://travis-ci.org/jorabin/KeePassJava2) -Develop: [![Build Status](https://travis-ci.org/jorabin/KeePassJava2.svg?branch=develop)](https://travis-ci.org/jorabin/KeePassJava2) -A Java 7 API for databases compatible with the renowned [KeePass](http://keepass.info) password +A Java 8 API for databases compatible with the renowned [KeePass](http://keepass.info) password safe for Windows. Features to date: -- Read and write KeePass 2.x format (File format V3 supported, V4 in development) +- Read and write KeePass 2.x format (File formats V3 and V4) - Keepass 2.x Password and Keyfile Credentials - Read KeePass 1.x format (Rijndael only) - *No* requirement for JCE Policy Files @@ -30,9 +28,23 @@ It is licensed under the Apache 2 License and is currently usable. (see [license](#license)) -## Maven Coordinates +## Current Status -The composite POM is +After a number of years of neglect, the project is (May 2023) back in development. + +The current code is version 3.2-SNAPSHOT. This is on the main branch. It has +not yet been distributed to Maven. See [Build from Source](#build-from-source) + +Key updates: +- Java 8 (dependencies no longer support Java 7) +- Updated dependencies to remove known vulnerabilities +- File format version 4 support - with Argon2 + +See the [changelog](CHANGELOG.md) for more details. + +## Maven Coordinates + +The composite POM for the last release (3.1), Java 7 compatible, is org.linguafranca.pwdb KeePassJava2 @@ -41,17 +53,17 @@ The composite POM is at Maven Central. Note that the artifactId has become Camel Case from release 2.1.x onwards. There are also separate POMs for the various modules. The module structure is illustrated below - under [Build from Source](#bfs). + under [Build from Source](#build-from-source). - Snapshot builds at [Sonatype OSS](https://oss.sonatype.org/content/groups/public/). + ## Java Version -It is written for Java 1.7. +From release 3.2 it requires Java 1.8. Earlier versions require Java 1.7. ## Quick Start -Create credentials and an input stream for the password file in question: +Create credentials and an input stream for the password vault in question: KdbxCreds creds = new KdbxCreds("123".getBytes()); InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test1.kdbx"); @@ -73,7 +85,7 @@ The table below illustrates timings for the file `test1.kdbx` (in the test module resources - it is around 2k bytes and contains a few dozen entries) as assessed by [this test](https://github.com/jorabin/KeePassJava2/blob/master/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java) - in the examples module. + in the "examples" module. Simple 5 loads 20 iterations 257 millis Jaxb 5 loads 20 iterations 326 millis @@ -96,7 +108,7 @@ Password databases are modelled as a three layer abstraction. A *Database* is a collection of records whose physical representation needs only to be capable of rendering as a stream. *Entries* hold the information of value in the database and *Groups* allow the structuring of entries into collections, just like a folder structure. -The Database has a root group and by following sub-groups of the root group the tree structure of the database can be navigated. Entries belong to groups. Entries can be moved between groups and groups can also be moved between groups. However, entries and groups created in one database cannot be moved to another database without being converted: +The Database has a root group and by following subgroups of the root group the tree structure of the database can be navigated. Entries belong to groups. Entries can be moved between groups and groups can also be moved between groups. However, entries and groups created in one database cannot be moved to another database without being converted: database.newEntry(entryToCopy); database.newGroup(groupToCopy); @@ -118,15 +130,17 @@ This project is so named by kind permission of Dominik Reichl the author of KeeP is no formal connection with that project. It has always been the intention to support other specific password database implementations. -Hence the creation of abstract Database interfaces rather than following the KeePass model +Hence, the creation of abstract Database interfaces rather than following the KeePass model exactly. KeePass is in effect defined by the code that Dominik writes to create and maintain the project. -Hence there is not much by way of definitive specification of KeePass files other than that code. There -is [a discussion](https://keepass.info/help/kb/kdbx_4.html) of the differences between KDBX version 3.1 and version 4. +Hence, there is not much by way of definitive specification of KeePass files other than that code. There +is a discussion of the [differences between KDBX version 3.1 and version 4](https://keepass.info/help/kb/kdbx_4.html). +There is also a discussion of the [enhancements in KDBX 4.1](https://keepass.info/help/kb/kdbx_4.1.html). For the sake of -clarification and my own satisfaction I have written about my understanding of KeePass formats in the following locations: +clarification and my own satisfaction I have written about my understanding of +KeePass formats in the following locations: 1. The Javadoc header to [KdbxSerializer](http://javadoc.io/page/org.linguafranca.pwdb/KeePassJava2-kdbx/latest/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.html) describes KDBX stream formatting. 2. The XSD Schema [KDBX.4.xsd](KDBX.4.xsd) documents my understanding of the Keepass XML, and also my lack of understanding, in parts. @@ -153,13 +167,13 @@ It also depends on SLF4J and Junit for tests. Included POM is for Maven 3. -### Module Structure +### Module Structure There are rather a lot of modules, this is in order to allow loading of minimal necessary functionality. The module dependencies are illustrated below. [![Module Structure](ModuleStructure.svg "Module Structure")](./ModuleStructure.svg) -Each module corresponds to a Maven artifact. The GroupId is `org.linguafranca.pwdb`. The version id is as noted [above](#mvn). +Each module corresponds to a Maven artifact. The GroupId is `org.linguafranca.pwdb`. The version id is as noted [above](#maven-coordinates). @@ -204,23 +218,10 @@ bindings might be useful for building other interfaces. - - - -
        Javadocs A DOM based implementation of KDBX. Being DOM based it is rather slow, but messes less with existing content than the other two implementations. Known to work on Android.
        httpkeepasshttpAn implementation of a server -intended to be the equivalent of -keepasshttp, which is a plugin for Windows Keepass -supporting communication with Chrome (chromeIPass) and Firefox(PassIFox) extensions. - - -**It is experimental and usafe.** - -Please -read and inwardly digest the readme.
        -Why are there so many implementations for KDBX? Well, the DOM implementation came first, because of the fact that it can +Why are there so many implementations for KDBX? Well, the DOM implementation came first, because it can load and save stuff that the implementation doesn't specifically know about. But it is very slow. Then came the JAXB implementation, but @@ -242,9 +243,9 @@ In [this file](./CHANGELOG.md). Many thanks to Pavel Ivanov [@ivanovpv](https://github.com/ivanovpv) for his help with Android and Gradle compatibility issues. -## License +## License -Copyright (c) 2016 Jo Rabin +Copyright (c) 2023 Jo Rabin Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 5cb83463cebc8cda042afd271f6155a19a864a8c Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 6 May 2023 17:31:43 +0100 Subject: [PATCH 084/228] Fix failure to read keyfile version 2 #38 --- .../pwdb/kdbx/dom/Issue38Test.java | 27 ++++++++++++++++++ .../linguafranca/pwdb/kdbx/KdbxKeyFile.java | 17 ++++++----- .../resources/issue-38/Database/Database.kdbx | Bin 0 -> 1870 bytes .../resources/issue-38/Database/Database.keyx | 12 ++++++++ 4 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/Issue38Test.java create mode 100644 test/src/main/resources/issue-38/Database/Database.kdbx create mode 100644 test/src/main/resources/issue-38/Database/Database.keyx diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/Issue38Test.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/Issue38Test.java new file mode 100644 index 00000000..a40568d0 --- /dev/null +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/Issue38Test.java @@ -0,0 +1,27 @@ +package org.linguafranca.pwdb.kdbx.dom; + +import org.junit.Test; +import org.linguafranca.pwdb.kdbx.KdbxCreds; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +/** + * Bug report on GitHub, the Keyfile is Version 2 (Hex) + */ +public class Issue38Test { + + @Test + public void testV2Keyfile() throws IOException { + InputStream databaseStream = Issue38Test.class.getClassLoader().getResourceAsStream("issue-38/Database/Database.kdbx"); + InputStream keyStream = Issue38Test.class.getClassLoader().getResourceAsStream("issue-38/Database/Database.keyx"); + assert keyStream != null; + KdbxCreds creds = new KdbxCreds("MyPassword".getBytes(), keyStream); + assert databaseStream != null; + DomDatabaseWrapper database = DomDatabaseWrapper.load(creds, databaseStream); + List entries = database.findEntries("Sample Entry"); + DomEntryWrapper entry = entries.get(0); + System.out.println(entry.getTitle()); + } +} diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java index 505f0dd8..9d2b52e5 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java @@ -17,6 +17,7 @@ package org.linguafranca.pwdb.kdbx; import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.Hex; import org.w3c.dom.Document; import javax.xml.parsers.DocumentBuilder; @@ -39,21 +40,23 @@ public class KdbxKeyFile { /** * Load a key from an InputStream with a KDBX XML key file. * @param inputStream the input stream holding the key - * @return they key or null if there was a problem + * @return the key */ public static byte[] load(InputStream inputStream) { - String base64; try { DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document doc = documentBuilder.parse(inputStream); - base64 = (String) xpath.evaluate("//KeyFile/Key/Data/text()", doc, XPathConstants.STRING); - if (base64 == null) { + String version = (String) xpath.evaluate("//KeyFile/Meta/Version/text()", doc, XPathConstants.STRING); + String data = (String) xpath.evaluate("//KeyFile/Key/Data/text()", doc, XPathConstants.STRING); + if (data == null) { return null; } + if (version.equals("2.0")) { + return Hex.decodeHex(data.replaceAll("\\s","")); + } + return Base64.decodeBase64(data.getBytes()); } catch (Exception e) { - return null; + throw new RuntimeException("Key File input stream cannot be null"); } - // android compatibility - return Base64.decodeBase64(base64.getBytes()); } } diff --git a/test/src/main/resources/issue-38/Database/Database.kdbx b/test/src/main/resources/issue-38/Database/Database.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..b769f15a72f8868fff0206e25fdc25a03f761e43 GIT binary patch literal 1870 zcmV-U2eJ4A*`k_f`%AR}00RI55CAd3^5(yBLr}h01tDtuTK@wC0096100bZa&u@5X zJ56nNT(Z1)ROV$*oUCVJ{3@5mvId5~1G&_B1t0(=;+HCu&y3)C+q!gytv{B?>cI&d z*w&t5v;9DDV?_Z52moN}00000000LN0HtQk3voPc-6|Ve_(x+PD+nL}GxX*MRa>2` zVaV}%2_OKXEdljTF}0+-MDdjJ5O`K8Tp!6ooBa45 z6O?lrO6-^l1ONg60000401XNa3JH=%pEdmw)rA6VRKhp-#rQ;?x?e=9m^`rOwrcR7 zv=*?{Rf_0VtkEVb{M}HDu$7^l?O9(KfF}ef1SYqL1tdM%u5BM#s}z^=;VmJMY3l*H z2;N@haj-Q&O8@^4`aFk<>)$%&%XQrsMVwlzYC|PVI+$b7ne?h}{pCD;9rz=`?G~bz z%#?%Qq|o9`h+^W@^dHNxF3zrCjPE2{sk1CW=V-6vJH8<$Y@ejb*jssRQYDTJpIdGP zL$E~}h4w4UZlNsjjK#S+C>^-}Xp*H^R*<9~KlFZu#li(^GX_*sxJyfSa_7I<^& zHTTIB4{;AQugq`GY^&91hqAupu%tzkm@AW>yyPB!YfL3nuo|7EPuxjR=tzvBv67Xh z|4p>rB+{L!3fu zjjcwYBFcws@qGof7l-Y01t9?4Wq#p+2QOd*mWqW%1di%P8N*^Unx8p=`43TNAL#)C zG@{aRbsf2{X_wtdt0+j}RziqUw43wvV_KgJ!+0zKSe>pNogA3K7x#u>#sh2_`=xEQ zWETb^xkC-so}P3KRC7voc=Xp&^UaAx}&pfaNu5jtZ zE7qbEM~blfMvK95wX`22J*lW+q((YTC@KM)-oU8g(-W=TJrhco_Vijqhpp{(HesG8 zPZNe%;3M@W5rp5iw{fe!Cv>`Y31TmloPDOb(sfWZolIAAO_3E~ICl6$B=+_&CK@%e zHXfH`7%-|Qh&_qe;xnVScSSs2HG63>0vvkeO~k$S9-E4*uWjvmN zJsH_Wh69dpf+NsXc7H@Vw$z&-y60le&aOdq+_ocUZr)q83bMQOwPLK9YLe2>()ZAK z93!3x8y4jSoEZG3z5&bSc?LT(1S=@szL||)*r)nirWmpC->a#i`zM}57^h4J7b0Rz z(!kkY_Aqo3Yd@at*XLsVW#znnr+JRI>!xd5mjq0sAw!|Mh(r$z8vT|nP`)6(ADXbh zn>)n#^IU3=q$TcsDBRnSfxkjiSw#JG3Fp6%UXw=!*FJO$jSL*8yv4U^g*7_=KLO*b&IFG=IjH@263fS8PEB+p0(m>k#Vo zkbWe<8`Mpb<<`gHaDUI8Ifn5Vry2OFLnH8*J(0P`?(Z&)O`9B9|S^&;bF*SvU`^ z3;fhjZZCBAWx4|q{H~UH;Y+uBc*nx#jXVV;=@))^{={q*vZ1nGXShL!SDQbcCo=OM Ivq8e}=~+^NzW@LL literal 0 HcmV?d00001 diff --git a/test/src/main/resources/issue-38/Database/Database.keyx b/test/src/main/resources/issue-38/Database/Database.keyx new file mode 100644 index 00000000..fe8be0de --- /dev/null +++ b/test/src/main/resources/issue-38/Database/Database.keyx @@ -0,0 +1,12 @@ + + + + 2.0 + + + + A700DD37 8EBAABE2 1936E1F9 9FED8DEE + D0F71664 B25B0DD2 961FFDA6 9E194E75 + + + \ No newline at end of file From af5407cd5b3c5ce0ccdec86d89d4dcbdaf8deb27 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sun, 7 May 2023 21:45:38 +0100 Subject: [PATCH 085/228] A pretty major hack through to implement support for saving with non-default encryption parameters #36 --- KDBX.4.xsd | 15 +- .../java/org/linguafranca/pwdb/Database.java | 15 +- .../pwdb}/SerializableDatabase.java | 25 +- .../pwdb/StreamConfiguration.java | 10 + .../org/linguafranca/pwdb}/StreamFormat.java | 22 +- .../org/linguafranca/pwdb/security/Aes.java | 7 +- .../linguafranca/pwdb/security/Argon2.java | 16 +- .../linguafranca/pwdb/security/ChaCha.java | 11 +- .../pwdb/security/CipherAlgorithm.java | 7 +- .../pwdb/security/Encryption.java | 62 +++- .../pwdb/security/KeyDerivationFunction.java | 7 +- .../pwdb/security}/StreamEncryptor.java | 3 +- .../pwdb/security/VariantDictionary.java | 26 +- .../pwdb/kdbx/dom/DomDatabaseWrapper.java | 28 +- .../linguafranca/pwdb/kdbx/dom/DomHelper.java | 26 +- .../kdbx/dom/DomSerializableDatabase.java | 62 ++-- .../kdbx/dom/DomBinaryPropertyV4Test.java | 2 +- .../pwdb/kdbx/dom/DomDatabaseWrapperTest.java | 2 +- .../pwdb/kdbx/dom/DomSaveAndReloadTest.java | 7 +- .../linguafranca/pwdb/kdbx/QuickStart.java | 109 +++++- .../org/linguafranca/pwdb/kdbx/SaxParse.java | 1 + .../pwdb/example/SimpleQuickStartTest.java | 22 +- .../pwdb/kdbx/jaxb/JaxbDatabase.java | 18 +- .../kdbx/jaxb/JaxbSerializableDatabase.java | 68 ++-- .../kdbx/jaxb/JaxbBinaryPropertyV4Test.java | 2 +- .../pwdb/kdbx/jaxb/JaxbSaveAndReloadTest.java | 1 - .../linguafranca/pwdb/kdb/KdbDatabase.java | 16 +- .../linguafranca/pwdb/kdbx/KdbxHeader.java | 125 ++++--- .../pwdb/kdbx/KdbxStreamFormat.java | 122 ++++--- ...erializerTest.java => KdbxHeaderTest.java} | 47 +-- .../pwdb/kdbx/KdbxSerializerTest.java | 30 +- .../pwdb/security/StreamEncryptionTest.java | 3 +- kdbx/src/test/resources/AES-AES.xml | 313 ----------------- kdbx/src/test/resources/AES-Argon2.xml | 266 --------------- kdbx/src/test/resources/ChaCha-AES.xml | 313 ----------------- .../pwdb/kdbx/simple/SimpleDatabase.java | 10 +- .../transformer/KdbxInputTransformer.java | 2 +- .../transformer/KdbxOutputTransformer.java | 2 +- .../pwdb/kdbx/KdbxInnerInputStreamTest.java | 1 + .../simple/SimpleBinaryPropertyV4Test.java | 2 +- .../pwdb/checks/BinaryPropertyChecks.java | 4 +- test/src/main/resources/V4-AES-AES.kdbx | Bin 1966 -> 2055 bytes test/src/main/resources/V4-ChaCha-AES.kdbx | Bin 1958 -> 0 bytes test/src/main/resources/V4-ChaCha20-AES.kdbx | Bin 0 -> 2039 bytes ...dbx => V4-ChaCha20-Argon2-Attachment.kdbx} | Bin .../resources/issue-38/Database/README.md | 1 + test/src/main/resources/xml/V4-AES-AES.xml | 319 ++++++++++++++++++ test/src/main/resources/xml/V4-AES-Argon2.xml | 266 +++++++++++++++ .../main/resources/xml/V4-ChaCha20-AES.xml | 319 ++++++++++++++++++ .../xml/V4-ChaCha20-Argon2-Attachment.xml | 154 +++++++++ 50 files changed, 1658 insertions(+), 1231 deletions(-) rename {kdbx/src/main/java/org/linguafranca/pwdb/kdbx => database/src/main/java/org/linguafranca/pwdb}/SerializableDatabase.java (74%) create mode 100644 database/src/main/java/org/linguafranca/pwdb/StreamConfiguration.java rename {kdbx/src/main/java/org/linguafranca/pwdb/kdbx => database/src/main/java/org/linguafranca/pwdb}/StreamFormat.java (77%) rename {kdbx/src/main/java/org/linguafranca/pwdb/kdbx => database/src/main/java/org/linguafranca/pwdb/security}/StreamEncryptor.java (97%) rename kdbx/src/test/java/org/linguafranca/pwdb/kdbx/{stream/KdbxSerializerTest.java => KdbxHeaderTest.java} (58%) delete mode 100644 kdbx/src/test/resources/AES-AES.xml delete mode 100644 kdbx/src/test/resources/AES-Argon2.xml delete mode 100644 kdbx/src/test/resources/ChaCha-AES.xml mode change 100755 => 100644 test/src/main/resources/V4-AES-AES.kdbx delete mode 100755 test/src/main/resources/V4-ChaCha-AES.kdbx create mode 100644 test/src/main/resources/V4-ChaCha20-AES.kdbx rename test/src/main/resources/{Attachment-ChaCha20-Argon2.kdbx => V4-ChaCha20-Argon2-Attachment.kdbx} (100%) create mode 100644 test/src/main/resources/issue-38/Database/README.md create mode 100644 test/src/main/resources/xml/V4-AES-AES.xml create mode 100644 test/src/main/resources/xml/V4-AES-Argon2.xml create mode 100644 test/src/main/resources/xml/V4-ChaCha20-AES.xml create mode 100644 test/src/main/resources/xml/V4-ChaCha20-Argon2-Attachment.xml diff --git a/KDBX.4.xsd b/KDBX.4.xsd index aca0cdef..bf25f2df 100644 --- a/KDBX.4.xsd +++ b/KDBX.4.xsd @@ -14,6 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> + @@ -78,10 +79,12 @@ When the XML database is loaded into memory, sensitive fields will be encrypted in that stream. This element specifies which of the standardFields are encrypted in that stream, which is decrypted using Salsa20 - and the InnerRandomStream encryption header. + for KDBX3 (or ChaCha20 by default for V4) and the InnerRandomStream encryption header. - /./Entry/String/Value/@Protected signifies that the value of a stringField is + ./Entry/String/Value/@Protected signifies that the value of a stringField is so encrypted and applies also to customFields. + + Attachments are optionally memory protected in V4. @@ -107,7 +110,7 @@ Used to store binary values in version 3.1 - in version 4 - such values are now stored as part of the KDBX inner headers. + such values are now transferred as part of the KDBX inner headers. @@ -306,7 +309,7 @@ - XSD doesn't do comma separated lists so we can't model this handily. + XSD doesn't do comma separated lists, so we can't model this handily. Assume that the format is something like [{token}{, token}*]+ where white space is ignored except as part of the name of the token. @@ -319,9 +322,9 @@ This is where the values of the database are actually stored. You can have String valued - fields and you can have Binary valued fields. There are "Default" String fields (username and so on) and + fields, and you can have Binary valued fields. There are "Default" String fields (username and so on) and there are custom string fields (custom only in that their names are not the names of default string - fields. Not really clear whether the keys are case sensitive. + fields). Not really clear whether the keys are case-sensitive. diff --git a/database/src/main/java/org/linguafranca/pwdb/Database.java b/database/src/main/java/org/linguafranca/pwdb/Database.java index 67295b56..dddac89b 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Database.java +++ b/database/src/main/java/org/linguafranca/pwdb/Database.java @@ -32,7 +32,7 @@ * to one database cannot in general be added to another database, they need to be * imported using {@link #newGroup(Group)} and {@link #newEntry(Entry)}, or implicitly * imported using {@link Group#addGroup(Group)} which automatically create Groups and - * Entries (as well as importing sub groups and their entries). {@link Group#addEntry(Entry)} + * Entries (as well as importing subgroups and their entries). {@link Group#addEntry(Entry)} * allows arbitrary importing from other databases. * *

        Databases may be navigated directly from the root {@link #getRootGroup()}, @@ -77,7 +77,7 @@ public interface Database , G extends Group group); /** * Create a new Entry @@ -157,8 +157,8 @@ public interface Database , G extends Group, G extends Group void save(StreamFormat streamFormat, Credentials credentials, OutputStream outputStream) throws IOException; + /** * Properties to encrypt * @param propertyName the property of interest diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/SerializableDatabase.java b/database/src/main/java/org/linguafranca/pwdb/SerializableDatabase.java similarity index 74% rename from kdbx/src/main/java/org/linguafranca/pwdb/kdbx/SerializableDatabase.java rename to database/src/main/java/org/linguafranca/pwdb/SerializableDatabase.java index 9ce4173a..1fd8d9e5 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/SerializableDatabase.java +++ b/database/src/main/java/org/linguafranca/pwdb/SerializableDatabase.java @@ -14,24 +14,27 @@ * limitations under the License. */ -package org.linguafranca.pwdb.kdbx; +package org.linguafranca.pwdb; + +import org.linguafranca.pwdb.security.StreamEncryptor; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** - * This interface allows for serialization and deserialization of KDBX databases. - * - *

        Databases instantiate themselves from a stream and serialize to a stream, + * This interface allows for serialization and deserialization of databases. + *

        + * Databases instantiate themselves from a stream and serialize to a stream, * and need to be able to encrypt and decrypt data (e.g. Protected fields in KDBX format). - * - *

        KDBX V3 databases contain a header hash (i.e. a hash of the contents of - * some portion of the {@link StreamFormat} they have been loaded from or saved to. + *

        + * KDBX V3 databases contain a header hash (i.e. a hash of the contents of + * some portion of the {@link StreamFormat} they have been loaded from or saved to). * Which means that databases must support the setting of this value after the header * has been written on save, and reading the value after load to allow for integrity checking. - * - * @author jo + *

        + * KDBX V4 databases have their attachments in the header so databases need to support setting + * and getting of attachments for serialization */ public interface SerializableDatabase { @@ -48,4 +51,8 @@ public interface SerializableDatabase { void setHeaderHash(byte[] hash); void addBinary(int index, byte[] payload); + + byte[] getBinary(int index); + + int getBinaryCount(); } diff --git a/database/src/main/java/org/linguafranca/pwdb/StreamConfiguration.java b/database/src/main/java/org/linguafranca/pwdb/StreamConfiguration.java new file mode 100644 index 00000000..4b717495 --- /dev/null +++ b/database/src/main/java/org/linguafranca/pwdb/StreamConfiguration.java @@ -0,0 +1,10 @@ +package org.linguafranca.pwdb; + +/** + * A marker interface for KdbxHeader and similar, if there ever is one + */ +public interface StreamConfiguration { + class None implements StreamConfiguration { + + } +} diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamFormat.java b/database/src/main/java/org/linguafranca/pwdb/StreamFormat.java similarity index 77% rename from kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamFormat.java rename to database/src/main/java/org/linguafranca/pwdb/StreamFormat.java index 4c7bbf24..bc8779bc 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamFormat.java +++ b/database/src/main/java/org/linguafranca/pwdb/StreamFormat.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.linguafranca.pwdb.kdbx; +package org.linguafranca.pwdb; -import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.security.StreamEncryptor; import java.io.IOException; import java.io.InputStream; @@ -27,11 +27,11 @@ * * @author jo */ -public interface StreamFormat { +public interface StreamFormat { /** * Class allows for serializing a database directly to or from a stream with no encryption etc */ - class None implements StreamFormat { + class None implements StreamFormat { @Override public void load(SerializableDatabase serializableDatabase, Credentials credentials, InputStream inputStream) throws IOException { @@ -47,9 +47,23 @@ public void save(SerializableDatabase serializableDatabase, Credentials credenti outputStream.flush(); outputStream.close(); } + + @Override + public StreamConfiguration.None getStreamConfiguration() { + return new StreamConfiguration.None(); + } + + @Override + public void setStreamConfiguration(StreamConfiguration.None configuration) { + + } } void load(SerializableDatabase serializableDatabase, Credentials credentials, InputStream encryptedInputStream) throws IOException; void save(SerializableDatabase serializableDatabase, Credentials credentials, OutputStream encryptedOutputStream) throws IOException; + + C getStreamConfiguration(); + + void setStreamConfiguration(C configuration); } diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java index 223136b6..5b6d3e3e 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java @@ -27,7 +27,8 @@ public class Aes implements CipherAlgorithm, KeyDerivationFunction { /** UUID specifying that AES is to be used as the Key Derivation Function in KDBX */ private static final UUID KDF = UUID.fromString("C9D9F39A-628A-4460-BF74-0D08C18A4FEA"); - private static VariantDictionary kdfParameters = new VariantDictionary((short) 1); + private static final String name = "AES"; + private static final VariantDictionary kdfParameters = new VariantDictionary((short) 1); static { kdfParameters.putUuid("$UUID", KDF); kdfParameters.putLong(ParamRounds, 6000L); @@ -65,6 +66,10 @@ public UUID getCipherUuid() { return CIPHER; } + public String getName(){ + return name; + } + @Override public InputStream getDecryptedInputStream(InputStream encryptedInputStream, byte[] key, byte[] iv) { final ParametersWithIV keyAndIV = new ParametersWithIV(new KeyParameter(key), iv); diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java b/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java index 47646509..ca6533e6 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java @@ -1,14 +1,13 @@ package org.linguafranca.pwdb.security; -import java.nio.charset.StandardCharsets; +import org.bouncycastle.crypto.generators.Argon2BytesGenerator; +import org.bouncycastle.crypto.params.Argon2Parameters; + import java.security.SecureRandom; import java.util.UUID; import static org.linguafranca.pwdb.security.Argon2.VariantDictionaryKeys.*; -import org.bouncycastle.crypto.generators.Argon2BytesGenerator; -import org.bouncycastle.crypto.params.Argon2Parameters; - /** * KDBX V4 files may use Argon2 for key derivation. @@ -22,6 +21,8 @@ public class Argon2 implements KeyDerivationFunction { */ private static final UUID argon2_kdf = UUID.fromString("EF636DDF-8C29-444B-91F7-A9A403E30A0C"); + private static final String name = "Argon2"; + /** * hide constructor */ @@ -55,6 +56,11 @@ public UUID getKdfUuid() { return argon2_kdf; } + @Override + public String getName() { + return name; + } + @Override public byte[] getTransformedKey(byte[] digest, VariantDictionary argonParameterKeys) { int version = argonParameterKeys.mustGet(paramVersion).asInteger(); @@ -87,7 +93,7 @@ public VariantDictionary createKdfParameters() { vd.putLong("I", 2); vd.putLong("M", 64 * 1024 * 1024); vd.putUuid("$UUID", Argon2.argon2_kdf); - vd.put("S", VariantDictionary.EntryType.ARRRAY, random.generateSeed(32)); + vd.put("S", VariantDictionary.EntryType.ARRAY, random.generateSeed(32)); return vd; } } diff --git a/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java b/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java index 2d475651..65ad1f5b 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java @@ -18,7 +18,9 @@ */ public class ChaCha implements CipherAlgorithm { - private static final UUID CHACHA_CIPHER = UUID.fromString("d6038a2b-8b6f-4cb5-a524-339a31dbb59a"); + private static final UUID CHA_CHA_20_CIPHER = UUID.fromString("d6038a2b-8b6f-4cb5-a524-339a31dbb59a"); + + private static final String name = "CHA_CHA_20"; // hide constructor to enforce singleton private ChaCha(){} @@ -30,7 +32,12 @@ public static ChaCha getInstance() { @Override public UUID getCipherUuid() { - return CHACHA_CIPHER; + return CHA_CHA_20_CIPHER; + } + + @Override + public String getName(){ + return name; } @Override diff --git a/database/src/main/java/org/linguafranca/pwdb/security/CipherAlgorithm.java b/database/src/main/java/org/linguafranca/pwdb/security/CipherAlgorithm.java index db7f396b..bb1a1c0a 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/CipherAlgorithm.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/CipherAlgorithm.java @@ -13,10 +13,15 @@ public interface CipherAlgorithm { */ UUID getCipherUuid(); + /** + * Return the name of this algorithm + */ + String getName(); + /** * Create a decrypted stream from the supplied encrypted one * - * @param encryptedInputStream an encryted stream + * @param encryptedInputStream an encrypted stream * @param key the decryption key * @param iv the iv * @return an unencrypted stream diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java b/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java index f99cfd62..3913f92c 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java @@ -16,6 +16,8 @@ package org.linguafranca.pwdb.security; +import org.linguafranca.pwdb.StreamFormat; + import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.InputStream; @@ -28,7 +30,7 @@ /** * Contains the key transform functions and cipher algorithms used in other modules. *

        - * Also some convenience utilities that hide the checked exceptions that would otherwise need to be checked for + * Also, some convenience utilities that hide the checked exceptions that would otherwise need to be checked for * when using digests. */ public class Encryption { @@ -132,6 +134,11 @@ public byte[] getTransformedKey(byte[] key, VariantDictionary transformParams) { public VariantDictionary createKdfParameters() { return kdf.createKdfParameters(); } + + @Override + public String getName(){ + return kdf.getName(); + } } /** @@ -139,7 +146,7 @@ public VariantDictionary createKdfParameters() { * Enum constants forward to underlying implementation. */ public enum Cipher implements CipherAlgorithm { - CHACHA(ChaCha.getInstance()), + CHA_CHA_20(ChaCha.getInstance()), AES(Aes.getInstance()); private final CipherAlgorithm ef; @@ -169,6 +176,11 @@ public UUID getCipherUuid() { return ef.getCipherUuid(); } + @Override + public String getName(){ + return this.ef.getName(); + } + @Override public InputStream getDecryptedInputStream(InputStream encryptedInputStream, byte[] key, byte[] iv) { return ef.getDecryptedInputStream(encryptedInputStream, key, iv); @@ -179,4 +191,50 @@ public OutputStream getEncryptedOutputStream(OutputStream decryptedOutputStream, return ef.getEncryptedOutputStream(decryptedOutputStream, key, iv); } } + + /** + * The ordinals represent various types of encryption that may + * be applied to fields within the unencrypted data + * + * @see StreamFormat + */ + @SuppressWarnings("WeakerAccess, unused") + public enum ProtectedStreamAlgorithm { + NONE(0), ARC_FOUR(1), SALSA_20(2), CHA_CHA_20(3); + + private final int value; + + ProtectedStreamAlgorithm(int value) { + this.value = value; + } + + public static ProtectedStreamAlgorithm getAlgorithm(int innerRandomStreamId) { + for (ProtectedStreamAlgorithm pse: values()) { + if (pse.value == innerRandomStreamId) { + return pse; + } + } + throw new IllegalArgumentException("Inner Random Stream Id " + innerRandomStreamId + "is not known"); + } + + public static StreamEncryptor getStreamEncryptor(ProtectedStreamAlgorithm psa, byte [] key) { + + switch (psa) { + case NONE: { + throw new IllegalStateException("Inner stream encoding of NONE"); + } + case ARC_FOUR: { + throw new UnsupportedOperationException("Arc Four inner stream not supported"); + } + case SALSA_20: { + return new StreamEncryptor.Salsa20(key); + } + case CHA_CHA_20: { + return new StreamEncryptor.ChaCha20(key); + } + } + throw new IllegalStateException("Inner stream encoding unsupported"); + } + + } } diff --git a/database/src/main/java/org/linguafranca/pwdb/security/KeyDerivationFunction.java b/database/src/main/java/org/linguafranca/pwdb/security/KeyDerivationFunction.java index d0d928bc..0e15663f 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/KeyDerivationFunction.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/KeyDerivationFunction.java @@ -12,7 +12,7 @@ public interface KeyDerivationFunction { UUID getKdfUuid(); /** - * Transform a key using this key derivation faunction + * Transform a key using this key derivation function * * @param key the key to transform * @param transformParams the parameters defining the way the transformation is to be carried out @@ -24,4 +24,9 @@ public interface KeyDerivationFunction { * Create default KDF parameters */ VariantDictionary createKdfParameters(); + + /** + * Get a name + */ + String getName(); } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamEncryptor.java b/database/src/main/java/org/linguafranca/pwdb/security/StreamEncryptor.java similarity index 97% rename from kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamEncryptor.java rename to database/src/main/java/org/linguafranca/pwdb/security/StreamEncryptor.java index d49c3ead..6fc32ed2 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamEncryptor.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/StreamEncryptor.java @@ -14,9 +14,8 @@ * limitations under the License. */ -package org.linguafranca.pwdb.kdbx; +package org.linguafranca.pwdb.security; -import org.linguafranca.pwdb.security.Encryption; import org.bouncycastle.crypto.StreamCipher; import org.bouncycastle.crypto.engines.ChaCha7539Engine; import org.bouncycastle.crypto.engines.Salsa20Engine; diff --git a/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java b/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java index 85ed015c..c9bcc7fb 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java @@ -39,7 +39,7 @@ public enum EntryType { INT32(0xC), INT64(0xD), STRING(0x18), // UTF-8, without BOM, without null terminator - ARRRAY(0x42); + ARRAY(0x42); private final byte value; @@ -62,14 +62,14 @@ public static EntryType get(byte type) { @Immutable public static class Entry { private final byte type; - private final @NotNull byte[] value; + private final byte @NotNull [] value; private final ByteOrder byteOrder; - public Entry(EntryType entryType, @NotNull byte[] value) { + public Entry(EntryType entryType, byte @NotNull [] value) { this(entryType, value, ByteOrder.LITTLE_ENDIAN); } - public Entry(EntryType entryType, @NotNull byte[] value, ByteOrder byteOrder) { + public Entry(EntryType entryType, byte @NotNull [] value, ByteOrder byteOrder) { this.type = entryType.value; this.value = checkNotNull(value, vnn); this.byteOrder = byteOrder; @@ -101,7 +101,7 @@ public int asInteger() { return ByteBuffer.wrap(value).order(byteOrder).getInt(); } - public @NotNull byte[] asByteArray() { + public byte @NotNull [] asByteArray() { return value; } } @@ -121,10 +121,8 @@ public VariantDictionary(short version) { */ public VariantDictionary copy() { VariantDictionary vd = new VariantDictionary(this.version); - for (Map.Entry e : this.entries.entrySet()) { - vd.entries.put(e.getKey(), e.getValue()); - } - return null; + vd.entries.putAll(this.entries); + return vd; } /** @@ -174,7 +172,7 @@ public short getVersion() { * @param type the data type of the entry * @param value a buffer containing an appropriate entry */ - public void put(@NotNull String key, EntryType type, @NotNull byte[] value) { + public void put(@NotNull String key, EntryType type, byte @NotNull [] value) { entries.put(checkNotNull(key), new Entry(type, checkNotNull(value))); } @@ -186,18 +184,18 @@ public void putUuid(@NotNull String key, UUID uuid) { ByteBuffer bb = ByteBuffer.wrap(buf); bb.putLong(0, uuid.getMostSignificantBits()); bb.putLong(8, uuid.getLeastSignificantBits()); - entries.put(checkNotNull(key, knn), new Entry(ARRRAY, buf)); + entries.put(checkNotNull(key, knn), new Entry(ARRAY, buf)); } /** * Put a byte array under the key defined */ - public void putByteArray(@NotNull String key, @NotNull byte[] value) { - entries.put(checkNotNull(key, knn), new Entry(ARRRAY, value)); + public void putByteArray(@NotNull String key, byte @NotNull [] value) { + entries.put(checkNotNull(key, knn), new Entry(ARRAY, value)); } /** - * Put a long as an signed64 under the key defined + * Put a long as a signed64 under the key defined */ public void putLong(@NotNull String key, long value) { byte[] buf = new byte[8]; diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java index 70e4b236..b783bbf7 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java @@ -17,10 +17,11 @@ package org.linguafranca.pwdb.kdbx.dom; import org.jetbrains.annotations.NotNull; +import org.linguafranca.pwdb.StreamConfiguration; import org.linguafranca.pwdb.base.AbstractDatabase; import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; -import org.linguafranca.pwdb.kdbx.StreamFormat; +import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.Credentials; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -47,14 +48,14 @@ public class DomDatabaseWrapper extends AbstractDatabase DomDatabaseWrapper (StreamFormat streamFormat, Credentials credentials, InputStream inputStream) throws IOException { streamFormat.load(domDatabase, credentials, inputStream); init(); } @@ -65,7 +66,12 @@ public static DomDatabaseWrapper load (@NotNull Credentials credentials, @NotNul checkNotNull(inputStream, "InputStream must not be null")); } - private void init() { + public static DomDatabaseWrapper load (StreamFormat streamFormat, @NotNull Credentials credentials, @NotNull InputStream inputStream) throws IOException { + return new DomDatabaseWrapper(streamFormat, credentials, inputStream); + } + + + private void init() { document = domDatabase.getDoc(); try { dbRootGroup = ((Element) DomHelper.xpath.evaluate("/KeePassFile/Root/Group", document, XPathConstants.NODE)); @@ -81,7 +87,7 @@ public void save(Credentials credentials, OutputStream outputStream) throws IOEx setDirty(false); } - public void save(StreamFormat streamFormat, Credentials credentials, OutputStream outputStream) throws IOException { + public void save(StreamFormat streamFormat, Credentials credentials, OutputStream outputStream) throws IOException { streamFormat.save(domDatabase, credentials, outputStream); setDirty(false); } @@ -91,7 +97,7 @@ public boolean shouldProtect(String name) { if (protectionElement == null) { return false; } - return Boolean.valueOf(protectionElement.getTextContent()); + return Boolean.parseBoolean(protectionElement.getTextContent()); } @Override @@ -123,9 +129,9 @@ public DomIconWrapper newIcon(Integer i) { @Override public DomGroupWrapper getRecycleBin() { - String UUIDcontent = getElementContent(RECYCLE_BIN_UUID_ELEMENT_NAME, dbMeta); - if (UUIDcontent != null){ - final UUID uuid = Helpers.uuidFromBase64(UUIDcontent); + String UuidContent = getElementContent(RECYCLE_BIN_UUID_ELEMENT_NAME, dbMeta); + if (UuidContent != null){ + final UUID uuid = Helpers.uuidFromBase64(UuidContent); if (uuid.getLeastSignificantBits() != 0 && uuid.getMostSignificantBits() != 0) { for (DomGroupWrapper g: getRootGroup().getGroups()) { if (g.getUuid().equals(uuid)) { @@ -151,7 +157,7 @@ public DomGroupWrapper getRecycleBin() { @Override public boolean isRecycleBinEnabled() { - return Boolean.valueOf(getElementContent(RECYCLE_BIN_ENABLED_ELEMENT_NAME, dbMeta)); + return Boolean.parseBoolean(getElementContent(RECYCLE_BIN_ENABLED_ELEMENT_NAME, dbMeta)); } @Override diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java index 12ded6c5..c09062cc 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java @@ -23,13 +23,10 @@ import org.w3c.dom.Element; import org.w3c.dom.NodeList; -//import javax.xml.bind.DatatypeConverter; - import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; -import java.text.SimpleDateFormat; import java.util.*; /** @@ -41,8 +38,6 @@ class DomHelper { static XPath xpath = XPathFactory.newInstance().newXPath(); -// static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"); - static final String GROUP_ELEMENT_NAME = "Group"; static final String ENTRY_ELEMENT_NAME = "Entry"; static final String ICON_ELEMENT_NAME = "IconID"; @@ -233,6 +228,27 @@ public static void addBinary(Element documentElement, String b64, Integer index) binary.setAttribute("ID", index.toString()); } + public static String getBinary(Element documentElement, Integer index) { + Element binaries = getElement("Meta/Binaries", documentElement,false); + if (Objects.isNull(binaries)){ + throw new IllegalArgumentException("No binaries found"); + } + for (int i = 0; i < binaries.getChildNodes().getLength(); i++){ + if (((Element) binaries.getChildNodes().item(i)).getAttribute("ID").equals(index.toString())) { + return ((Element) binaries.getChildNodes().item(i)).getTextContent(); + } + } + throw new IllegalArgumentException("No binary with that index found"); + } + + public static int getBinaryCount(Element documentElement) { + Element binaries = getElement("Meta/Binaries", documentElement,false); + if (Objects.isNull(binaries)) { + return 0; + } + return binaries.getChildNodes().getLength(); + } + @NotNull static Element touchElement(String elementPath, Element parentElement) { return setElementContent(elementPath, parentElement, Helpers.fromDate(new Date())); diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java index 4ababed0..c5f1e6a8 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java @@ -18,8 +18,8 @@ import org.linguafranca.pwdb.kdbx.Helpers; -import org.linguafranca.pwdb.kdbx.SerializableDatabase; -import org.linguafranca.pwdb.kdbx.StreamEncryptor; +import org.linguafranca.pwdb.SerializableDatabase; +import org.linguafranca.pwdb.security.StreamEncryptor; import org.apache.commons.codec.binary.Base64; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -38,9 +38,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.security.SecureRandom; -import java.text.ParseException; -import java.util.Date; +import java.nio.charset.StandardCharsets; /** * This class is an XML DOM implementation of a KDBX database. The data is maintained as a DOM, @@ -59,19 +57,15 @@ public class DomSerializableDatabase implements SerializableDatabase { private DomSerializableDatabase() {} - public static DomSerializableDatabase createEmptyDatabase() throws IOException { + public static DomSerializableDatabase createEmptyDatabase() { DomSerializableDatabase result = new DomSerializableDatabase(); // read in the template KeePass XML database - result.load(result.getClass().getClassLoader().getResourceAsStream("base.kdbx.xml")); try { - // replace all placeholder dates with now (this is now already done in the loader) -/* - String now = DomHelper.dateFormatter.format(new Date()); - NodeList list = (NodeList) DomHelper.xpath.evaluate("//*[contains(text(),'${creationDate}')]", result.doc.getDocumentElement(), XPathConstants.NODESET); - for (int i = 0; i < list.getLength(); i++) { - list.item(i).setTextContent(now); - } -*/ + result.load(result.getClass().getClassLoader().getResourceAsStream("base.kdbx.xml")); + } catch (IOException e) { + throw new RuntimeException(e); + } + try { // set the root group UUID Node uuid = (Node) DomHelper.xpath.evaluate("//"+ DomHelper.UUID_ELEMENT_NAME, result.doc.getDocumentElement(), XPathConstants.NODE); uuid.setTextContent(DomHelper.base64RandomUuid()); @@ -96,7 +90,7 @@ public SerializableDatabase load(InputStream inputStream) throws IOException { String base64 = DomHelper.getElementContent(".", element); // Android compatibility byte[] encrypted = Base64.decodeBase64(base64.getBytes()); - String decrypted = new String(encryption.decrypt(encrypted), "UTF-8"); + String decrypted = new String(encryption.decrypt(encrypted), StandardCharsets.UTF_8); DomHelper.setElementContent(".", element, decrypted); element.removeAttribute("Protected"); } @@ -112,38 +106,21 @@ public SerializableDatabase load(InputStream inputStream) throws IOException { } } -/* - public void dateConvert() { - try { - // finding all elements name ending Changed and Time - NodeList dateContent = (NodeList) DomHelper.xpath.evaluate("//*[substring(local-name(), string-length(local-name()) -3) = 'Time']", doc, XPathConstants.NODESET); - processDates(dateContent); - - dateContent = (NodeList) DomHelper.xpath.evaluate("//*[substring(local-name(), string-length(local-name()) -6) = 'Changed']", doc, XPathConstants.NODESET); - processDates(dateContent); - } catch (XPathExpressionException e) { - throw new IllegalStateException(e); - } - } -*/ - @Override public void addBinary(int index, byte[] payload) { DomHelper.addBinary(doc.getDocumentElement(), Helpers.encodeBase64Content(payload, true),index); } -/* - private void processDates(NodeList dateContent) { - Date now = new Date(); - for (int i = 0; i < dateContent.getLength(); i++){ - Element element = ((Element) dateContent.item(i)); - String content = DomHelper.getElementContent(".", element); - Date d = content == null || content.equals("${creationDate}") ? now :Helpers.toDate(content); - DomHelper.setElementContent(".", element, DomHelper.dateFormatter.format(d)); - } + @Override + public byte[] getBinary(int index) { + return Helpers.decodeBase64Content(DomHelper.getBinary(doc.getDocumentElement(), index).getBytes(StandardCharsets.UTF_8), true); + } + + @Override + public int getBinaryCount() { + return DomHelper.getBinaryCount(doc.getDocumentElement()); } -*/ @Override public void save(OutputStream outputStream) { Document copyDoc = (Document) doc.cloneNode(true); @@ -180,6 +157,7 @@ public void save(OutputStream outputStream) { //factory.setAttribute("indent-number", "4"); Transformer transformer = factory.newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + //noinspection HttpUrlsUsage transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); transformer.transform(xmlSource, outputTarget); } catch (TransformerException e) { @@ -192,7 +170,7 @@ public void save(OutputStream outputStream) { private void prepareProtection(Document doc, String protect) throws XPathExpressionException { // does this require encryption String query = String.format(protectQuery, protect); - if (!((String) DomHelper.xpath.evaluate(query, doc, XPathConstants.STRING)).toLowerCase().equals("true")) { + if (!((String) DomHelper.xpath.evaluate(query, doc, XPathConstants.STRING)).equalsIgnoreCase("true")) { return; } // mark the field as Protected but don't actually encrypt yet, that comes later diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV4Test.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV4Test.java index bae3ff76..422ffa24 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV4Test.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV4Test.java @@ -31,7 +31,7 @@ public class DomBinaryPropertyV4Test extends BinaryPropertyChecks { public DomBinaryPropertyV4Test() throws IOException { - InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment-ChaCha20-Argon2.kdbx"); + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha20-Argon2-Attachment.kdbx"); database = DomDatabaseWrapper.load(new KdbxCreds("123".getBytes()), inputStream); } diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java index c7612b1b..e6c20a0d 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java @@ -21,7 +21,7 @@ import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; -import org.linguafranca.pwdb.kdbx.StreamFormat; +import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.Credentials; import java.io.IOException; diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomSaveAndReloadTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomSaveAndReloadTest.java index 75752e10..a7a75ba5 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomSaveAndReloadTest.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomSaveAndReloadTest.java @@ -19,7 +19,6 @@ import org.linguafranca.pwdb.checks.SaveAndReloadChecks; import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.kdbx.KdbxCreds; -import org.linguafranca.pwdb.kdbx.StreamFormat; import org.linguafranca.pwdb.Credentials; import java.io.IOException; @@ -33,11 +32,7 @@ public class DomSaveAndReloadTest extends SaveAndReloadChecks { @Override public Database getDatabase() { - try { - return new DomDatabaseWrapper(); - } catch (IOException e) { - throw new IllegalStateException(); - } + return new DomDatabaseWrapper(); } @Override public Database getDatabase(String name, Credentials credentials) throws IOException { diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java index cde5c12a..81496d43 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java @@ -20,15 +20,18 @@ import org.linguafranca.pwdb.*; import org.linguafranca.pwdb.kdb.KdbCredentials; import org.linguafranca.pwdb.kdb.KdbDatabase; -import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.kdbx.dom.DomDatabaseWrapper; +import org.linguafranca.pwdb.security.Aes; +import org.linguafranca.pwdb.security.Encryption; +import org.linguafranca.pwdb.security.KeyDerivationFunction; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; import java.nio.file.Files; import java.nio.file.Paths; import java.util.List; +import static org.junit.Assert.assertEquals; + /** * Examples for QuickStart * @@ -127,7 +130,6 @@ public void loadKdb() throws IOException { // load KdbDatabase KdbDatabase database = KdbDatabase.load(credentials, inputStream); // visit all groups and entries and list them to console - database.visit(new Visitor.Print()); // create a KDBX (database D kdbxDatabase = getDatabase(); @@ -140,4 +142,101 @@ public void loadKdb() throws IOException { kdbxDatabase.save(new KdbxCreds("123".getBytes()), f); } } + + /** + * Load KDBX V3 save as KDBX V4 - then load again and save with different configuration + */ + public void loadKdbx3SaveKdbx4() throws IOException { + DomDatabaseWrapper database; + // password credentials + KdbxCreds credentials = new KdbxCreds("123".getBytes()); + + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test123.kdbx")) { + // load KdbDatabase + database = DomDatabaseWrapper.load(credentials, inputStream); + } + // visit all groups and entries and list them to console + database.visit(new Visitor.Print()); + + // create a KDBX (database + D kdbxDatabase = getDatabase(); + kdbxDatabase.setName("New Database"); + kdbxDatabase.setDescription("Migration of KDB Database to KDBX Database"); + // deep copy from group (not including source group, KDB database has simulated root) + kdbxDatabase.getRootGroup().copy(database.getRootGroup()); + + // choose a stream format - V4 Kdbx and choose some algorithms + KdbxStreamFormat formatV4 = new KdbxStreamFormat(new KdbxHeader(KdbxHeader.KdbxHeaderOpts.V4_AES_ARGON_CHA_CHA)); + KdbxHeader kdbxHeader = formatV4.getStreamConfiguration(); + // change algos from those originally selected + kdbxHeader.setCipherAlgorithm(Encryption.Cipher.CHA_CHA_20); + kdbxHeader.setKeyDerivationFunction(Encryption.Kdf.AES); + kdbxHeader.setProtectedStreamAlgorithm(Encryption.ProtectedStreamAlgorithm.CHA_CHA_20); + + // save it with format options + try (FileOutputStream f = new FileOutputStream("testOutput/CHACHA-AES-CHACHA.kdbx")) { + kdbxDatabase.save(formatV4, credentials, f); + } + + // doesn't matter what we create it will be overwritten + KdbxStreamFormat kdbxStreamFormat = new KdbxStreamFormat(); + // load it again + try (FileInputStream f = new FileInputStream("testOutput/CHACHA-AES-CHACHA.kdbx")) { + DomDatabaseWrapper.load(kdbxStreamFormat, credentials, f); + } + + assertEquals("CHA_CHA_20", kdbxStreamFormat.getStreamConfiguration().getCipherAlgorithm().getName()); + assertEquals("AES", kdbxStreamFormat.getStreamConfiguration().getKeyDerivationFunction().getName()); + assertEquals("CHA_CHA_20", kdbxStreamFormat.getStreamConfiguration().getProtectedStreamAlgorithm().name()); + + try (FileOutputStream f = new FileOutputStream("testOutput/CHACHA-AES-CHACHA-2.kdbx")) { + kdbxDatabase.save(kdbxStreamFormat, credentials, f); + } + + // doesn't matter what we create it will be overwritten + KdbxStreamFormat kdbxStreamFormat2 = new KdbxStreamFormat(); + // load it again + try (FileInputStream f = new FileInputStream("testOutput/CHACHA-AES-CHACHA-2.kdbx")) { + DomDatabaseWrapper.load(kdbxStreamFormat2, credentials, f); + } + // still CHA-CHA_20 etc. + assertEquals("CHA_CHA_20", kdbxStreamFormat2.getStreamConfiguration().getCipherAlgorithm().getName()); + assertEquals("AES", kdbxStreamFormat.getStreamConfiguration().getKeyDerivationFunction().getName()); + assertEquals("CHA_CHA_20", kdbxStreamFormat.getStreamConfiguration().getProtectedStreamAlgorithm().name()); + } + + + public KdbxHeader loadKdbxHeader(String filename) throws IOException { + KdbxHeader kdbxHeader = new KdbxHeader(); + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(filename)) { + try { + // load KdbDatabase with no credentials, in V4 this will fail as HMac256 won't be correct + // but the header will still be populated + KdbxSerializer.createUnencryptedInputStream(new Credentials.None(), kdbxHeader, inputStream); + } catch (IllegalStateException ignored) { + } + return kdbxHeader; + } + } + + public void listKdbxHeaderProperties(KdbxHeader kdbxHeader, PrintWriter printWriter) { + printWriter.format("Version: %d\n", kdbxHeader.getVersion()); + printWriter.format("Cipher Algorithm: %s\n", kdbxHeader.getCipherAlgorithm().getName()); + // AES is the only KDF in V3 + KeyDerivationFunction kdf = kdbxHeader.getVersion() == 3 ? Aes.getInstance() : kdbxHeader.getKeyDerivationFunction(); + printWriter.format("Key Derivation Function: %s\n", kdf.getName()); + printWriter.format("Inner Stream Algorithm: %s\n", kdbxHeader.getProtectedStreamAlgorithm().name()); + printWriter.flush(); + } + /** + * List Database Encryption Characteristics + */ + public void listKdbxHeaderParams () throws IOException { + PrintWriter writer = new PrintWriter(System.out); + listKdbxHeaderProperties(loadKdbxHeader("V4-AES-AES.kdbx"), writer); + listKdbxHeaderProperties(loadKdbxHeader("V4-AES-Argon2.kdbx"), writer); + listKdbxHeaderProperties(loadKdbxHeader("V4-ChaCha20-AES.kdbx"), writer); + listKdbxHeaderProperties(loadKdbxHeader("V4-ChaCha20-Argon2-Attachment.kdbx"), writer); + } + } diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java index 2ac0d480..554916a0 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java @@ -17,6 +17,7 @@ package org.linguafranca.pwdb.kdbx; import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.security.StreamEncryptor; import org.xml.sax.*; import javax.xml.parsers.ParserConfigurationException; diff --git a/example/src/test/java/org/linguafranca/pwdb/example/SimpleQuickStartTest.java b/example/src/test/java/org/linguafranca/pwdb/example/SimpleQuickStartTest.java index 07b08797..d92511c0 100644 --- a/example/src/test/java/org/linguafranca/pwdb/example/SimpleQuickStartTest.java +++ b/example/src/test/java/org/linguafranca/pwdb/example/SimpleQuickStartTest.java @@ -19,6 +19,10 @@ import org.junit.Ignore; import org.junit.Test; import org.linguafranca.pwdb.kdbx.QuickStart; +import org.linguafranca.pwdb.kdbx.dom.DomDatabaseWrapper; +import org.linguafranca.pwdb.kdbx.dom.DomEntryWrapper; +import org.linguafranca.pwdb.kdbx.dom.DomGroupWrapper; +import org.linguafranca.pwdb.kdbx.dom.DomIconWrapper; import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; import org.linguafranca.pwdb.kdbx.simple.SimpleEntry; import org.linguafranca.pwdb.kdbx.simple.SimpleGroup; @@ -33,18 +37,18 @@ * * @author jo */ -public class SimpleQuickStartTest extends QuickStart { +public class SimpleQuickStartTest extends QuickStart { @Override - public SimpleDatabase getDatabase() { - return new SimpleDatabase(); + public DomDatabaseWrapper getDatabase() { + return new DomDatabaseWrapper(); } @Override - public SimpleDatabase loadDatabase(Credentials credentials, InputStream inputStream){ + public DomDatabaseWrapper loadDatabase(Credentials credentials, InputStream inputStream){ try { - return SimpleDatabase.load(credentials, inputStream); + return DomDatabaseWrapper.load(credentials, inputStream); } catch (Exception e) { throw new IllegalStateException(e); } @@ -65,5 +69,13 @@ public void loadKdbSaveVernacular() throws IOException { super.loadKdb(); } + @Test + public void loadSaveLoadSave() throws IOException { + loadKdbx3SaveKdbx4(); + } + @Test + public void listKdbxParamsTest() throws IOException { + listKdbxHeaderParams(); + } } diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java index 53d17e0a..bf471700 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java @@ -18,11 +18,10 @@ import org.jetbrains.annotations.NotNull; import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.StreamConfiguration; +import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.base.AbstractDatabase; -import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; -import org.linguafranca.pwdb.kdbx.StreamFormat; -import org.linguafranca.pwdb.kdbx.jaxb.binding.Binaries; import org.linguafranca.pwdb.kdbx.jaxb.binding.KeePassFile; import org.linguafranca.pwdb.kdbx.jaxb.binding.ObjectFactory; @@ -40,9 +39,9 @@ public class JaxbDatabase extends AbstractDatabase { - private KeePassFile keePassFile; - private ObjectFactory objectFactory = new ObjectFactory(); - private JaxbGroup root; + private final KeePassFile keePassFile; + private final ObjectFactory objectFactory = new ObjectFactory(); + private final JaxbGroup root; public JaxbDatabase() { this(createEmptyDatabase().getKeePassFile()); @@ -61,12 +60,11 @@ public static JaxbDatabase createEmptyDatabase() { } public static JaxbDatabase load(Credentials creds, InputStream inputStream) { - StreamFormat format = new KdbxStreamFormat(); - return load(format, creds, inputStream); + return load(new KdbxStreamFormat(), creds, inputStream); } @NotNull - public static JaxbDatabase load(StreamFormat format, Credentials creds, InputStream inputStream) { + public static JaxbDatabase load(StreamFormat format, Credentials creds, InputStream inputStream) { JaxbSerializableDatabase db = new JaxbSerializableDatabase(); try { format.load(db, creds, inputStream); @@ -81,7 +79,7 @@ public void save(Credentials creds, OutputStream outputStream) throws IOExceptio save(new KdbxStreamFormat(), creds, outputStream); } - public void save(StreamFormat format, Credentials creds, OutputStream outputStream) throws IOException { + public void save(StreamFormat format, Credentials creds, OutputStream outputStream) throws IOException { JaxbSerializableDatabase jsd = new JaxbSerializableDatabase(); jsd.setKeePassFile(this.keePassFile); format.save(jsd, creds, outputStream); diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java index d93bf99c..e228a7ce 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java @@ -17,10 +17,10 @@ package org.linguafranca.pwdb.kdbx.jaxb; import org.apache.commons.codec.binary.Base64; +import org.linguafranca.pwdb.SerializableDatabase; import org.linguafranca.pwdb.kdbx.Helpers; -import org.linguafranca.pwdb.kdbx.SerializableDatabase; -import org.linguafranca.pwdb.kdbx.StreamEncryptor; import org.linguafranca.pwdb.kdbx.jaxb.binding.*; +import org.linguafranca.pwdb.security.StreamEncryptor; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; @@ -29,7 +29,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -41,7 +41,7 @@ public class JaxbSerializableDatabase implements SerializableDatabase { protected KeePassFile keePassFile; private StreamEncryptor encryption; - private ObjectFactory objectFactory = new ObjectFactory(); + private final ObjectFactory objectFactory = new ObjectFactory(); @Override @@ -52,24 +52,20 @@ public JaxbSerializableDatabase load(InputStream inputStream) { u.setListener(new Unmarshaller.Listener() { @Override public void afterUnmarshal(Object target, Object parent) { - try { - if (target instanceof StringField.Value) { - StringField.Value value = (StringField.Value) target; - if (value.getProtected() !=null && value.getProtected()) { - byte[] encrypted = Base64.decodeBase64(value.getValue().getBytes()); - String decrypted = new String(encryption.decrypt(encrypted), "UTF-8"); - value.setValue(decrypted); - value.setProtected(false); - } + if (target instanceof StringField.Value) { + StringField.Value value = (StringField.Value) target; + if (value.getProtected() !=null && value.getProtected()) { + byte[] encrypted = Base64.decodeBase64(value.getValue().getBytes()); + String decrypted = new String(encryption.decrypt(encrypted), StandardCharsets.UTF_8); + value.setValue(decrypted); + value.setProtected(false); } - if (target instanceof JaxbGroupBinding && (parent instanceof JaxbGroupBinding)) { - ((JaxbGroupBinding) target).parent = ((JaxbGroupBinding) parent); - } - if (target instanceof JaxbEntryBinding && (parent instanceof JaxbGroupBinding)) { - ((JaxbEntryBinding) target).parent = ((JaxbGroupBinding) parent); - } - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException(); + } + if (target instanceof JaxbGroupBinding && (parent instanceof JaxbGroupBinding)) { + ((JaxbGroupBinding) target).parent = ((JaxbGroupBinding) parent); + } + if (target instanceof JaxbEntryBinding && (parent instanceof JaxbGroupBinding)) { + ((JaxbEntryBinding) target).parent = ((JaxbGroupBinding) parent); } } }); @@ -105,18 +101,14 @@ public void save(OutputStream outputStream) throws IOException { u.setListener(new Marshaller.Listener() { @Override public void beforeMarshal(Object source) { - try { - if (source instanceof StringField) { - StringField field = (StringField) source; - if (toEncrypt.contains(field.getKey())) { - byte[] encrypted = encryption.encrypt(field.getValue().getValue().getBytes()); - String b64 = new String(Base64.encodeBase64(encrypted), "UTF-8"); - field.getValue().setValue(b64); - field.getValue().setProtected(true); - } + if (source instanceof StringField) { + StringField field = (StringField) source; + if (toEncrypt.contains(field.getKey())) { + byte[] encrypted = encryption.encrypt(field.getValue().getValue().getBytes()); + String b64 = new String(Base64.encodeBase64(encrypted), StandardCharsets.UTF_8); + field.getValue().setValue(b64); + field.getValue().setProtected(true); } - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException(); } } }); @@ -151,6 +143,16 @@ public void addBinary(int index, byte[] value) { addBinary(keePassFile, objectFactory, index, value); } + @Override + public byte[] getBinary(int index) { + return keePassFile.getMeta().getBinaries().getBinary().get(index).getValue(); + } + + @Override + public int getBinaryCount() { + return keePassFile.getMeta().getBinaries().getBinary().size(); + } + public static void addBinary(KeePassFile keePassFile, ObjectFactory objectFactory, int index, byte[] value) { // create a new binary to put in the store Binaries.Binary newBin = objectFactory.createBinariesBinary(); @@ -163,6 +165,8 @@ public static void addBinary(KeePassFile keePassFile, ObjectFactory objectFactor keePassFile.getMeta().getBinaries().getBinary().add(newBin); } + + public KeePassFile getKeePassFile() { return keePassFile; } diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.java index 940707b6..97e96dbb 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.java @@ -33,7 +33,7 @@ public class JaxbBinaryPropertyV4Test extends BinaryPropertyChecks { public JaxbBinaryPropertyV4Test() throws IOException { - InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment-ChaCha20-Argon2.kdbx"); + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha20-Argon2-Attachment.kdbx"); database = JaxbDatabase.load(new KdbxCreds("123".getBytes()), inputStream); } diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSaveAndReloadTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSaveAndReloadTest.java index 7c5d2a9b..0c3680e6 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSaveAndReloadTest.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSaveAndReloadTest.java @@ -19,7 +19,6 @@ import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.checks.SaveAndReloadChecks; import org.linguafranca.pwdb.kdbx.KdbxCreds; -import org.linguafranca.pwdb.kdbx.StreamFormat; import org.linguafranca.pwdb.Credentials; import java.io.IOException; diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbDatabase.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbDatabase.java index 30c1295f..a9ca71ed 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbDatabase.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbDatabase.java @@ -16,12 +16,8 @@ package org.linguafranca.pwdb.kdb; -import org.linguafranca.pwdb.Group; -import org.linguafranca.pwdb.Icon; -import org.linguafranca.pwdb.Visitor; +import org.linguafranca.pwdb.*; import org.linguafranca.pwdb.base.AbstractDatabase; -import org.linguafranca.pwdb.Credentials; -import org.linguafranca.pwdb.Entry; import java.io.IOException; import java.io.InputStream; @@ -36,7 +32,7 @@ */ public class KdbDatabase extends AbstractDatabase { private String description; - private KdbGroup rootGroup; + private final KdbGroup rootGroup; static SimpleDateFormat isoDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); @@ -103,10 +99,16 @@ public void setDescription(String description) { } @Override - public void save(Credentials credentials, OutputStream outputStream) throws IOException { + public void save(Credentials credentials, OutputStream outputStream) { throw new UnsupportedOperationException("Cannot save KDB files in this implementation"); } + @Override + public void save(StreamFormat streamFormat, Credentials credentials, + OutputStream outputStream) { + throw new UnsupportedOperationException(); + } + @Override public boolean shouldProtect(String propertyName) { return false; diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java index f914d8f1..e7adc544 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java @@ -17,6 +17,7 @@ package org.linguafranca.pwdb.kdbx; import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.StreamConfiguration; import org.linguafranca.pwdb.security.*; import javax.crypto.Mac; @@ -44,8 +45,7 @@ * While KDBX streams are Little-Endian, data is passed to and from this class in standard Java byte order. */ @SuppressWarnings("WeakerAccess") -public class KdbxHeader { - +public class KdbxHeader implements StreamConfiguration { /** * The ordinal 0 represents uncompressed and 1 GZip compressed */ @@ -54,33 +54,6 @@ public enum CompressionFlags { NONE, GZIP } - /** - * The ordinals represent various types of encryption that may - * be applied to fields within the unencrypted data - * - * @see StreamFormat - * @see KdbxStreamFormat - */ - @SuppressWarnings("WeakerAccess, unused") - public enum ProtectedStreamAlgorithm { - NONE(0), ARC_FOUR(1), SALSA_20(2), CHA_CHA_20(3); - - private final int value; - - ProtectedStreamAlgorithm(int value) { - this.value = value; - } - - public static ProtectedStreamAlgorithm getAlgorithm(int innerRandomStreamId) { - for (ProtectedStreamAlgorithm pse: values()) { - if (pse.value == innerRandomStreamId) { - return pse; - } - } - throw new IllegalArgumentException("Inner Random Stream Id " + innerRandomStreamId + "is not known"); - } - } - private final List allowableVersions = new ArrayList<>(Arrays.asList(3, 4)); @@ -100,7 +73,9 @@ public static ProtectedStreamAlgorithm getAlgorithm(int innerRandomStreamId) { /* header (V3) inner header (v4) */ private byte[] innerRandomStreamKey; - private ProtectedStreamAlgorithm protectedStreamAlgorithm; + private Encryption.ProtectedStreamAlgorithm protectedStreamAlgorithm; + private CipherAlgorithm cipherAlgorithm; + private KeyDerivationFunction keyDerivationFunction; /* these bytes appear in cipher text immediately following the header (V3) */ private byte[] streamStartBytes; @@ -124,6 +99,7 @@ public static ProtectedStreamAlgorithm getAlgorithm(int innerRandomStreamId) { /* the bytes that compose the outer header, required for V4 to calculate the HMac */ private byte[] headerBytes; + final SecureRandom random; /** * Provides for choice of version number and crypto options for V3 and V4 */ @@ -131,24 +107,24 @@ interface KdbxHeaderOptions { int getVersion(); CipherAlgorithm getCipherAlgorithm(); KeyDerivationFunction getKeyDerivationFunction(); - ProtectedStreamAlgorithm getProtectedStreamAlgorithm(); + Encryption.ProtectedStreamAlgorithm getProtectedStreamAlgorithm(); } /** * Default values for crypto options */ enum KdbxHeaderOpts implements KdbxHeaderOptions{ - V3_AES_SALSA_20(3, Encryption.Cipher.AES, Encryption.Kdf.AES, ProtectedStreamAlgorithm.SALSA_20), - V4_AES_ARGON_CHA_CHA (4, Encryption.Cipher.AES, Encryption.Kdf.ARGON2, ProtectedStreamAlgorithm.CHA_CHA_20); + V3_AES_SALSA_20(3, Encryption.Cipher.AES, Encryption.Kdf.AES, Encryption.ProtectedStreamAlgorithm.SALSA_20), + V4_AES_ARGON_CHA_CHA (4, Encryption.Cipher.AES, Encryption.Kdf.ARGON2, Encryption.ProtectedStreamAlgorithm.CHA_CHA_20); // final int version; final CipherAlgorithm algorithm; final KeyDerivationFunction kdf; - final ProtectedStreamAlgorithm protectedStreamAlgorithm; + final Encryption.ProtectedStreamAlgorithm protectedStreamAlgorithm; - KdbxHeaderOpts(int version, Encryption.Cipher cipher, Encryption.Kdf kdf, ProtectedStreamAlgorithm protectedStreamAlgorithm) { + KdbxHeaderOpts(int version, Encryption.Cipher cipher, Encryption.Kdf kdf, Encryption.ProtectedStreamAlgorithm protectedStreamAlgorithm) { this.version = version; this.algorithm = cipher; this.kdf = kdf; @@ -171,7 +147,7 @@ public KeyDerivationFunction getKeyDerivationFunction() { } @Override - public ProtectedStreamAlgorithm getProtectedStreamAlgorithm() { + public Encryption.ProtectedStreamAlgorithm getProtectedStreamAlgorithm() { return protectedStreamAlgorithm; } // @@ -193,10 +169,14 @@ public KdbxHeader(int version) { public KdbxHeader(KdbxHeaderOptions opts) { - SecureRandom random = new SecureRandom(); + random = new SecureRandom(); this.version = opts.getVersion(); + setCipherAlgorithm(opts.getCipherAlgorithm()); + setKeyDerivationFunction(opts.getKeyDerivationFunction()); + setProtectedStreamAlgorithm(opts.getProtectedStreamAlgorithm()); cipherUuid = opts.getCipherAlgorithm().getCipherUuid(); + compressionFlags = CompressionFlags.GZIP; masterSeed = random.generateSeed(32); transformSeed = random.generateSeed(32); @@ -204,21 +184,16 @@ public KdbxHeader(KdbxHeaderOptions opts) { encryptionIv = random.generateSeed(16); innerRandomStreamKey = random.generateSeed(32); streamStartBytes = new byte[32]; - this.protectedStreamAlgorithm = opts.getProtectedStreamAlgorithm(); - - kdfParameters = opts.getKeyDerivationFunction().createKdfParameters(); } /** * Compute the Hmac Key Digest - * KdbxFile.cs Computekeys + * from "KdbxFile.cs Computekeys" * * @param credentials the credentials * @return the digest */ public byte[] getHmacKey(Credentials credentials) { - // Compute the Hmac Key Digest - // KdbxFile.cs Computekeys MessageDigest md = Encryption.getSha512MessageDigestInstance(); md.update(getMasterSeed()); md.update(getTransformedKeyDigest(credentials.getKey())); @@ -289,7 +264,7 @@ public OutputStream createEncryptedStream(byte[] digest, OutputStream outputStre MessageDigest md = getSha256MessageDigestInstance(); md.update(masterSeed); byte[] finalKeyDigest = md.digest(getTransformedKeyDigest(digest)); - return Aes.getInstance().getEncryptedOutputStream(outputStream, finalKeyDigest, getEncryptionIv()); + return cipherAlgorithm.getEncryptedOutputStream(outputStream, finalKeyDigest, getEncryptionIv()); } // @@ -331,7 +306,15 @@ public byte[] getStreamStartBytes() { return streamStartBytes; } - public ProtectedStreamAlgorithm getProtectedStreamAlgorithm() { + public CipherAlgorithm getCipherAlgorithm() { + return cipherAlgorithm; + } + + public KeyDerivationFunction getKeyDerivationFunction() { + return keyDerivationFunction; + } + + public Encryption.ProtectedStreamAlgorithm getProtectedStreamAlgorithm() { return protectedStreamAlgorithm; } @@ -349,21 +332,7 @@ public VariantDictionary getKdfParameters() { } public StreamEncryptor getStreamEncryptor() { - switch (getProtectedStreamAlgorithm()) { - case NONE: { - throw new IllegalStateException("Inner stream encoding of NONE"); - } - case ARC_FOUR: { - throw new UnsupportedOperationException("Arc Four inner stream not supported"); - } - case SALSA_20: { - return new StreamEncryptor.Salsa20(this.innerRandomStreamKey); - } - case CHA_CHA_20: { - return new StreamEncryptor.ChaCha20(this.innerRandomStreamKey); - } - } - throw new IllegalStateException("Inner stream encoding unsupported"); + return Encryption.ProtectedStreamAlgorithm.getStreamEncryptor(getProtectedStreamAlgorithm(), this.innerRandomStreamKey); } public void setCompressionFlags(int flags) { @@ -395,20 +364,43 @@ public void setStreamStartBytes(byte[] streamStartBytes) { } public void setInnerRandomStreamId(int innerRandomStreamId) { - this.protectedStreamAlgorithm = ProtectedStreamAlgorithm.getAlgorithm(innerRandomStreamId); + this.protectedStreamAlgorithm = Encryption.ProtectedStreamAlgorithm.getAlgorithm(innerRandomStreamId); } - public void setHeaderHash(byte[] headerHash) { - this.headerHash = headerHash; + public void setProtectedStreamAlgorithm(Encryption.ProtectedStreamAlgorithm protectedStreamAlgorithm) { + this.protectedStreamAlgorithm = protectedStreamAlgorithm; } + public void setKeyDerivationFunction(KeyDerivationFunction keyDerivationFunction) { + this.keyDerivationFunction = keyDerivationFunction; + if (version > 3) { + kdfParameters = this.keyDerivationFunction.createKdfParameters(); + } + } + + public void setCipherAlgorithm(CipherAlgorithm cipherAlgorithm) { + this.cipherAlgorithm = cipherAlgorithm; + setCipherUuid(this.cipherAlgorithm.getCipherUuid()); + if (cipherAlgorithm.getName().equals("CHA_CHA_20")){ + encryptionIv = random.generateSeed(12); + } + } public void setCipherUuid(byte[] uuid) { ByteBuffer b = ByteBuffer.wrap(uuid); UUID incoming = new UUID(b.getLong(), b.getLong(8)); - if (!incoming.equals(Aes.getInstance().getCipherUuid()) && !incoming.equals(ChaCha.getInstance().getCipherUuid())) { - throw new IllegalStateException("Unknown Cipher UUID " + incoming); + setCipherUuid(incoming); + this.cipherAlgorithm = Encryption.Cipher.getCipherAlgorithm(incoming); + } + + public void setCipherUuid(UUID uuid) { + if (!uuid.equals(Aes.getInstance().getCipherUuid()) && !uuid.equals(ChaCha.getInstance().getCipherUuid())) { + throw new IllegalStateException("Unknown Cipher UUID " + uuid); } - this.cipherUuid = incoming; + this.cipherUuid = uuid; + } + + public void setHeaderHash(byte[] headerHash) { + this.headerHash = headerHash; } public void setVersion(int version) { @@ -423,6 +415,7 @@ public void setVersion(int version) { */ public void setKdfParameters(VariantDictionary kdfParameters) { this.kdfParameters = kdfParameters; + this.keyDerivationFunction = Encryption.Kdf.getKdf(kdfParameters.get("$UUID").asUuid()); } /** diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java index ab8248de..517752b6 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java @@ -17,6 +17,8 @@ package org.linguafranca.pwdb.kdbx; import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.SerializableDatabase; +import org.linguafranca.pwdb.StreamFormat; import java.io.IOException; import java.io.InputStream; @@ -25,72 +27,104 @@ /** * This class implements KDBX formatted saving and loading of databases - * */ -public class KdbxStreamFormat implements StreamFormat { - - private final Version version; - - public enum Version { - KDBX31(3), - KDBX4(4); - - private final int version; - - Version(int num) { - this.version = num; - } - - int getVersionNum() { - return this.version; - } - } +public class KdbxStreamFormat implements StreamFormat { + private KdbxHeader kdbxHeader; /** - * Create a StreamFormat for reading or for writing v3 + * Create a StreamFormat for reading or for writing v3 with default KdbxHeader */ public KdbxStreamFormat() { - this.version = Version.KDBX31; + this(new KdbxHeader(3)); } /** - * Specify a version for writing + * Specify a version for writing using default KdbxHeader + * * @param version the version + * @deprecated use {@link #KdbxStreamFormat(KdbxHeader)} with version set */ + @Deprecated public KdbxStreamFormat(Version version) { - this.version = version; + this(version.getVersionNum() == 3? new KdbxHeader(): new KdbxHeader(4)); + } + + /** + * Provide a {@link KdbxHeader} which includes version info + * @param kdbxHeader a {@link KdbxHeader} to specify (write) or (capture) read the file config + */ + + public KdbxStreamFormat(KdbxHeader kdbxHeader) { + this.kdbxHeader = kdbxHeader; } @Override public void load(SerializableDatabase serializableDatabase, Credentials credentials, InputStream encryptedInputStream) throws IOException { - KdbxHeader kdbxHeader = new KdbxHeader(); - InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, kdbxHeader, encryptedInputStream); - serializableDatabase.setEncryption(kdbxHeader.getStreamEncryptor()); - serializableDatabase.load(decryptedInputStream); - if (kdbxHeader.getVersion() == 3 && !Arrays.equals(serializableDatabase.getHeaderHash(), kdbxHeader.getHeaderHash())) { - throw new IllegalStateException("Header hash does not match"); + try (InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, kdbxHeader, encryptedInputStream)) { + serializableDatabase.setEncryption(kdbxHeader.getStreamEncryptor()); + serializableDatabase.load(decryptedInputStream); + if (kdbxHeader.getVersion() == 3 && !Arrays.equals(serializableDatabase.getHeaderHash(), kdbxHeader.getHeaderHash())) { + throw new IllegalStateException("Header hash does not match"); + } + if (kdbxHeader.getVersion() == 4) { + int count = 0; + for (byte[] binary : kdbxHeader.getBinaries()) { + serializableDatabase.addBinary(count, Arrays.copyOfRange(binary, 1, binary.length)); + count++; + } + } } + } + + @Override + public void save(SerializableDatabase serializableDatabase, Credentials credentials, OutputStream encryptedOutputStream) throws IOException { if (kdbxHeader.getVersion() == 4) { - int count = 0; - for (byte[] binary: kdbxHeader.getBinaries()) { - serializableDatabase.addBinary(count, Arrays.copyOfRange(binary,1, binary.length)); - count++; + for (int a = 0; a < serializableDatabase.getBinaryCount(); a++) { + int attachmentLength = serializableDatabase.getBinary(a).length; + byte[] binary = new byte[attachmentLength + 1]; + binary[0] = 0; + System.arraycopy(serializableDatabase.getBinary(a),0, binary, 1, attachmentLength); + kdbxHeader.addBinary(binary); + } + } + + try (OutputStream unencrytedOutputStream = KdbxSerializer.createEncryptedOutputStream(credentials, kdbxHeader, encryptedOutputStream)) { + if (kdbxHeader.getVersion() == 3) { + serializableDatabase.setHeaderHash(kdbxHeader.getHeaderHash()); } + serializableDatabase.setEncryption(kdbxHeader.getStreamEncryptor()); + serializableDatabase.save(unencrytedOutputStream); + unencrytedOutputStream.flush(); } - decryptedInputStream.close(); } @Override - public void save(SerializableDatabase serializableDatabase, Credentials credentials, OutputStream encryptedOutputStream) throws IOException { - // fresh kdbx header - KdbxHeader kdbxHeader = new KdbxHeader(version.getVersionNum()); - OutputStream unencrytedOutputStream = KdbxSerializer.createEncryptedOutputStream(credentials, kdbxHeader, encryptedOutputStream); - if (version == Version.KDBX31) { - serializableDatabase.setHeaderHash(kdbxHeader.getHeaderHash()); + public KdbxHeader getStreamConfiguration() { + return kdbxHeader; + } + + @Override + public void setStreamConfiguration(KdbxHeader configuration) { + this.kdbxHeader = configuration; + } + + /** + * KDBX file format specifier + * @deprecated use constructor with {@link KdbxHeader#setVersion(int)} + */ + @Deprecated + public enum Version { + KDBX31(3), + KDBX4(4); + + private final int version; + + Version(int num) { + this.version = num; + } + + int getVersionNum() { + return this.version; } - serializableDatabase.setEncryption(kdbxHeader.getStreamEncryptor()); - serializableDatabase.save(unencrytedOutputStream); - unencrytedOutputStream.flush(); - unencrytedOutputStream.close(); } } diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxHeaderTest.java similarity index 58% rename from kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java rename to kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxHeaderTest.java index 9a2005d9..56bf2780 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/stream/KdbxSerializerTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxHeaderTest.java @@ -1,4 +1,4 @@ -package org.linguafranca.pwdb.kdbx.stream; +package org.linguafranca.pwdb.kdbx; import com.google.common.io.CharStreams; import com.google.common.io.LittleEndianDataInputStream; @@ -19,7 +19,7 @@ /** * test decryption of various kinds */ -public class KdbxSerializerTest { +public class KdbxHeaderTest { @Test @Ignore // can be used for detailed HMAC debugging public void getHmacStream() throws IOException { @@ -36,50 +36,31 @@ public void getHmacStream() throws IOException { // check the correct version @Test - public void loadAesheader() throws IOException { + public void loadAesAesHeader() throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-AES.kdbx"); KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); - assertEquals(3, header.getVersion()); + assertEquals(4, header.getVersion()); } - // check correct version v4 + // check correct version @Test - public void loadArgonheader() throws IOException { + public void loadAesArgonHeader() throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); assertEquals(4, header.getVersion()); } - /** - * Check correct content against previously extracted content - */ @Test - public void loadChaChaAesHeader() throws IOException { - check("ChaCha-AES.xml","V4-ChaCha-AES.kdbx"); - } - - /** - * Check correct content against previously extracted content - */ - @Test - public void getDecryptedArgonInputStream() throws IOException { - check("AES-Argon2.xml","V4-AES-Argon2.kdbx"); + public void loadChaChaHeader() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha20-AES.kdbx"); + KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); + assertEquals(4, header.getVersion()); } - - /** - * Check correct content against previously extracted content - */ @Test - public void getDecryptedAesInputStream() throws IOException { - check("AES-AES.xml", "V4-AES-AES.kdbx"); + public void loadChaChaArgonHeader() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha20-Argon2-Attachment.kdbx"); + KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); + assertEquals(4, header.getVersion()); } - private void check(String compare, String file) throws IOException { - InputStream inputStream = getClass().getClassLoader().getResourceAsStream(file); - InputStream is = KdbxSerializer.createUnencryptedInputStream(new KdbxCreds("123".getBytes()), new KdbxHeader(), inputStream); - String actual = CharStreams.toString(new InputStreamReader(is, StandardCharsets.UTF_8)); - //noinspection DataFlowIssue - String expected = CharStreams.toString(new InputStreamReader(KdbxSerializerTest.class.getClassLoader().getResourceAsStream(compare))); - assertEquals(expected, actual); - } } \ No newline at end of file diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java index ce1c7905..fa2ebbcc 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java @@ -16,10 +16,12 @@ package org.linguafranca.pwdb.kdbx; +import com.google.common.io.CharStreams; import org.junit.Test; import org.linguafranca.pwdb.Credentials; import java.io.*; +import java.nio.charset.StandardCharsets; import java.util.Scanner; import static org.junit.Assert.assertEquals; @@ -37,14 +39,21 @@ public void testGetPlainTextInputStream() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment.kdbx"); Credentials credentials = new KdbxCreds("123".getBytes()); InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); - byte[] buffer = new byte[1024]; - while ( decryptedInputStream.available() > 0) { - int read = decryptedInputStream.read(buffer); - if (read == -1) break; - System.out.write(buffer, 0, read); - } + System.out.println(CharStreams.toString(new InputStreamReader(decryptedInputStream, StandardCharsets.UTF_8))); } + /** + * Test that we can read a kdbx v4 file and list the XML to console + */ + @Test + public void testGetPlainTextInputStream2() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha20-AES.kdbx"); + Credentials credentials = new KdbxCreds("123".getBytes()); + InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); + System.out.println(CharStreams.toString(new InputStreamReader(decryptedInputStream, StandardCharsets.UTF_8))); + } + + /** * Test that we can write a KDBX 3 file containing "Hello World" * and then read it back to get the same content @@ -81,8 +90,11 @@ public void testCypherTextOutputStream2() throws Exception { outputStream.flush(); outputStream.close(); - InputStream inputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), new FileInputStream(tempFile)); - Scanner scanner = new Scanner(inputStream); - assertEquals("Hello World", scanner.nextLine()); + KdbxHeader kdbxHeader1 = new KdbxHeader(); + try (InputStream inputStream = KdbxSerializer.createUnencryptedInputStream(credentials, kdbxHeader1, new FileInputStream(tempFile))){ + assertEquals(4, kdbxHeader1.getVersion()); + Scanner scanner = new Scanner(inputStream); + assertEquals("Hello World", scanner.nextLine()); + } } } \ No newline at end of file diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java index 62df3993..48014e8b 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java @@ -17,8 +17,7 @@ package org.linguafranca.pwdb.security; import org.junit.Test; -import org.linguafranca.pwdb.kdbx.StreamEncryptor; -import org.linguafranca.pwdb.kdbx.StreamEncryptor.*; +import org.linguafranca.pwdb.security.StreamEncryptor.*; import java.security.SecureRandom; diff --git a/kdbx/src/test/resources/AES-AES.xml b/kdbx/src/test/resources/AES-AES.xml deleted file mode 100644 index f77f9e1b..00000000 --- a/kdbx/src/test/resources/AES-AES.xml +++ /dev/null @@ -1,313 +0,0 @@ - - - - KeePass - V2s6F1AQsmPRea/yyukPa+C2kFstdTA+zcJBgeIgKDg= - New V4 Database - 2018-01-25T13:28:56Z - This is a new database - 2018-01-25T13:28:56Z - - 2018-01-25T13:24:05Z - 365 - - 2018-01-25T13:24:05Z - -1 - -1 - - False - False - True - False - False - - True - AAAAAAAAAAAAAAAAAAAAAA== - 2018-01-25T13:24:05Z - AAAAAAAAAAAAAAAAAAAAAA== - 2018-01-25T13:24:05Z - 10 - 6291456 - 3mzTUVoVAkO7DFaZ6Soxrw== - 3mzTUVoVAkO7DFaZ6Soxrw== - - - - - - 3mzTUVoVAkO7DFaZ6Soxrw== - NewV4Database - - 49 - - 2018-01-25T13:24:05Z - 2018-01-25T13:24:05Z - 2018-01-25T13:29:25Z - 2018-01-25T13:23:08Z - False - 4 - 2018-01-25T13:24:05Z - - True - - null - null - t53t29Y1A0abjVS1+xTo5g== - - t53t29Y1A0abjVS1+xTo5g== - 0 - - - - - - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:23:08Z - False - 0 - 2018-01-25T13:28:56Z - - - Notes - Notes - - - Password - U6biOGfHsEc= - - - Title - Sample Entry - - - URL - https://keepass.info/ - - - UserName - User Name - - - True - 0 - - Target Window - {USERNAME}{TAB}{PASSWORD}{TAB}{ENTER} - - - - - - YeputjepZky3vXID4/HHrA== - 0 - - - - - - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:23:08Z - False - 0 - 2018-01-25T13:28:56Z - - - Password - jywfUBg= - - - Title - Sample Entry #2 - - - URL - https://keepass.info/help/kb/testform.html - - - UserName - Michael321 - - - True - 0 - - *Test Form - KeePass* - - - - - - - TyxuU/7td0iU8+b0iHRh3w== - 0 - - - - - - 2018-01-26T13:20:58Z - 2018-01-25T13:28:56Z - 2018-01-26T13:22:20Z - 2018-01-25T13:23:08Z - False - 1 - 2018-01-26T13:20:58Z - - - Notes - - - - Password - BRBPZpg= - - - Title - Sample Entry #2 - Copy - - - URL - https://keepass.info/help/kb/testform.html - - - UserName - Michael321 - - - True - 0 - - *Test Form - KeePass* - - - - - - - 3BxuF+oUekuMpLe67xmTLQ== - General - - 48 - - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:29:24Z - 2018-01-25T13:23:08Z - False - 2 - 2018-01-25T13:28:56Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - vQsKl6dJiEiNvbTkjDVHcw== - Windows - - 38 - - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:23:08Z - False - 0 - 2018-01-25T13:28:56Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - 6bNmbKf/QUKN7I3GlNqqQw== - Network - - 3 - - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:23:08Z - False - 0 - 2018-01-25T13:28:56Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - Qz8rqQvYZUummUrakV6BFQ== - Internet - - 1 - - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:23:08Z - False - 0 - 2018-01-25T13:28:56Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - WMP+6xrurE+Jr6kFDwJtxQ== - eMail - - 19 - - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:23:08Z - False - 0 - 2018-01-25T13:28:56Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - mSY4yshaIU64BNreFBm+1w== - Homebanking - - 37 - - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:29:23Z - 2018-01-25T13:23:08Z - False - 1 - 2018-01-25T13:28:56Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - - - \ No newline at end of file diff --git a/kdbx/src/test/resources/AES-Argon2.xml b/kdbx/src/test/resources/AES-Argon2.xml deleted file mode 100644 index c488f75e..00000000 --- a/kdbx/src/test/resources/AES-Argon2.xml +++ /dev/null @@ -1,266 +0,0 @@ - - - - KeePass - mNL70Q4AAAA= - New V4 Database - mNL70Q4AAAA= - This is a new database - mNL70Q4AAAA= - - ddH70Q4AAAA= - 365 - - ddH70Q4AAAA= - -1 - -1 - - False - False - True - False - False - - True - AAAAAAAAAAAAAAAAAAAAAA== - ddH70Q4AAAA= - AAAAAAAAAAAAAAAAAAAAAA== - ddH70Q4AAAA= - 10 - 6291456 - 3mzTUVoVAkO7DFaZ6Soxrw== - 3mzTUVoVAkO7DFaZ6Soxrw== - - - - - 3mzTUVoVAkO7DFaZ6Soxrw== - NewV4Database - - 49 - - ddH70Q4AAAA= - ddH70Q4AAAA= - tdL70Q4AAAA= - PNH70Q4AAAA= - False - 4 - ddH70Q4AAAA= - - True - - null - null - t53t29Y1A0abjVS1+xTo5g== - - t53t29Y1A0abjVS1+xTo5g== - 0 - - - - - - mNL70Q4AAAA= - mNL70Q4AAAA= - mNL70Q4AAAA= - PNH70Q4AAAA= - False - 0 - mNL70Q4AAAA= - - - Notes - Notes - - - Password - 6f9J8T82DJ0= - - - Title - Sample Entry - - - URL - https://keepass.info/ - - - UserName - User Name - - - True - 0 - - Target Window - {USERNAME}{TAB}{PASSWORD}{TAB}{ENTER} - - - - - - YeputjepZky3vXID4/HHrA== - 0 - - - - - - mNL70Q4AAAA= - mNL70Q4AAAA= - mNL70Q4AAAA= - PNH70Q4AAAA= - False - 0 - mNL70Q4AAAA= - - - Password - heKymtY= - - - Title - Sample Entry #2 - - - URL - https://keepass.info/help/kb/testform.html - - - UserName - Michael321 - - - True - 0 - - *Test Form - KeePass* - - - - - - - 3BxuF+oUekuMpLe67xmTLQ== - General - - 48 - - mNL70Q4AAAA= - mNL70Q4AAAA= - tNL70Q4AAAA= - PNH70Q4AAAA= - False - 2 - mNL70Q4AAAA= - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - vQsKl6dJiEiNvbTkjDVHcw== - Windows - - 38 - - mNL70Q4AAAA= - mNL70Q4AAAA= - mNL70Q4AAAA= - PNH70Q4AAAA= - False - 0 - mNL70Q4AAAA= - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - 6bNmbKf/QUKN7I3GlNqqQw== - Network - - 3 - - mNL70Q4AAAA= - mNL70Q4AAAA= - mNL70Q4AAAA= - PNH70Q4AAAA= - False - 0 - mNL70Q4AAAA= - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - Qz8rqQvYZUummUrakV6BFQ== - Internet - - 1 - - mNL70Q4AAAA= - mNL70Q4AAAA= - mNL70Q4AAAA= - PNH70Q4AAAA= - False - 0 - mNL70Q4AAAA= - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - WMP+6xrurE+Jr6kFDwJtxQ== - eMail - - 19 - - mNL70Q4AAAA= - mNL70Q4AAAA= - mNL70Q4AAAA= - PNH70Q4AAAA= - False - 0 - mNL70Q4AAAA= - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - mSY4yshaIU64BNreFBm+1w== - Homebanking - - 37 - - mNL70Q4AAAA= - mNL70Q4AAAA= - s9L70Q4AAAA= - PNH70Q4AAAA= - False - 1 - mNL70Q4AAAA= - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - - - \ No newline at end of file diff --git a/kdbx/src/test/resources/ChaCha-AES.xml b/kdbx/src/test/resources/ChaCha-AES.xml deleted file mode 100644 index f0a7968f..00000000 --- a/kdbx/src/test/resources/ChaCha-AES.xml +++ /dev/null @@ -1,313 +0,0 @@ - - - - KeePass - qiv6mbGxmbZVNGs/0A7xbFUe9D8SxOcP+7U71ju4gAU= - New V4 Database - 2018-01-25T13:28:56Z - This is a new database - 2018-01-25T13:28:56Z - - 2018-01-25T13:24:05Z - 365 - - 2018-01-25T13:24:05Z - -1 - -1 - - False - False - True - False - False - - True - AAAAAAAAAAAAAAAAAAAAAA== - 2018-01-25T13:24:05Z - AAAAAAAAAAAAAAAAAAAAAA== - 2018-01-25T13:24:05Z - 10 - 6291456 - 3mzTUVoVAkO7DFaZ6Soxrw== - 3mzTUVoVAkO7DFaZ6Soxrw== - - - - - - 3mzTUVoVAkO7DFaZ6Soxrw== - NewV4Database - - 49 - - 2018-01-25T13:24:05Z - 2018-01-25T13:24:05Z - 2018-01-25T13:29:25Z - 2018-01-25T13:23:08Z - False - 4 - 2018-01-25T13:24:05Z - - True - - null - null - t53t29Y1A0abjVS1+xTo5g== - - t53t29Y1A0abjVS1+xTo5g== - 0 - - - - - - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:23:08Z - False - 0 - 2018-01-25T13:28:56Z - - - Notes - Notes - - - Password - hkDT7ZRetpo= - - - Title - Sample Entry - - - URL - https://keepass.info/ - - - UserName - User Name - - - True - 0 - - Target Window - {USERNAME}{TAB}{PASSWORD}{TAB}{ENTER} - - - - - - YeputjepZky3vXID4/HHrA== - 0 - - - - - - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:23:08Z - False - 0 - 2018-01-25T13:28:56Z - - - Password - Ep/7NYY= - - - Title - Sample Entry #2 - - - URL - https://keepass.info/help/kb/testform.html - - - UserName - Michael321 - - - True - 0 - - *Test Form - KeePass* - - - - - - - TyxuU/7td0iU8+b0iHRh3w== - 0 - - - - - - 2018-01-26T13:20:58Z - 2018-01-25T13:28:56Z - 2018-01-26T13:22:20Z - 2018-01-25T13:23:08Z - False - 1 - 2018-01-26T13:20:58Z - - - Notes - - - - Password - /+19knE= - - - Title - Sample Entry #2 - Copy - - - URL - https://keepass.info/help/kb/testform.html - - - UserName - Michael321 - - - True - 0 - - *Test Form - KeePass* - - - - - - - 3BxuF+oUekuMpLe67xmTLQ== - General - - 48 - - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:29:24Z - 2018-01-25T13:23:08Z - False - 2 - 2018-01-25T13:28:56Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - vQsKl6dJiEiNvbTkjDVHcw== - Windows - - 38 - - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:23:08Z - False - 0 - 2018-01-25T13:28:56Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - 6bNmbKf/QUKN7I3GlNqqQw== - Network - - 3 - - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:23:08Z - False - 0 - 2018-01-25T13:28:56Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - Qz8rqQvYZUummUrakV6BFQ== - Internet - - 1 - - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:23:08Z - False - 0 - 2018-01-25T13:28:56Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - WMP+6xrurE+Jr6kFDwJtxQ== - eMail - - 19 - - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:23:08Z - False - 0 - 2018-01-25T13:28:56Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - mSY4yshaIU64BNreFBm+1w== - Homebanking - - 37 - - 2018-01-25T13:28:56Z - 2018-01-25T13:28:56Z - 2018-01-25T13:29:23Z - 2018-01-25T13:23:08Z - False - 1 - 2018-01-25T13:28:56Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - - - \ No newline at end of file diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java index 67d0713b..be71e6f4 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java @@ -16,9 +16,11 @@ package org.linguafranca.pwdb.kdbx.simple; +import org.linguafranca.pwdb.StreamConfiguration; +import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.base.AbstractDatabase; import org.linguafranca.pwdb.kdbx.Helpers; -import org.linguafranca.pwdb.kdbx.StreamEncryptor; +import org.linguafranca.pwdb.security.StreamEncryptor; import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxInputTransformer; import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxOutputTransformer; import org.linguafranca.pwdb.kdbx.KdbxHeader; @@ -271,6 +273,12 @@ public void save(Credentials credentials, OutputStream outputStream) throws IOEx } } + @Override + public void save(StreamFormat streamFormat, Credentials credentials, + OutputStream outputStream) throws IOException{ + throw new UnsupportedOperationException(); + } + @Override public boolean shouldProtect(String s) { return keePassFile.meta.memoryProtection.shouldProtect(s); diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxInputTransformer.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxInputTransformer.java index 6175a0f1..9da9f498 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxInputTransformer.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxInputTransformer.java @@ -17,7 +17,7 @@ package org.linguafranca.pwdb.kdbx.simple.transformer; import org.linguafranca.pwdb.kdbx.Helpers; -import org.linguafranca.pwdb.kdbx.StreamEncryptor; +import org.linguafranca.pwdb.security.StreamEncryptor; import org.linguafranca.xml.XmlEventTransformer; import javax.xml.namespace.QName; diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxOutputTransformer.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxOutputTransformer.java index 52690ec4..28fa3aaa 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxOutputTransformer.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxOutputTransformer.java @@ -17,7 +17,7 @@ package org.linguafranca.pwdb.kdbx.simple.transformer; import org.linguafranca.pwdb.kdbx.Helpers; -import org.linguafranca.pwdb.kdbx.StreamEncryptor; +import org.linguafranca.pwdb.security.StreamEncryptor; import org.linguafranca.xml.XmlEventTransformer; import javax.xml.namespace.QName; diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java index 98f1bcf3..94c7380d 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java @@ -18,6 +18,7 @@ import org.junit.Test; import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxInputTransformer; +import org.linguafranca.pwdb.security.StreamEncryptor; import org.linguafranca.xml.XmlInputStreamFilter; import javax.xml.stream.XMLStreamException; diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java index c94ee8a4..3ac501d9 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java @@ -33,7 +33,7 @@ public class SimpleBinaryPropertyV4Test extends BinaryPropertyChecks { public SimpleBinaryPropertyV4Test() throws Exception { - InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment-ChaCha20-Argon2.kdbx"); + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha20-Argon2-Attachment.kdbx"); database = SimpleDatabase.load(new KdbxCreds("123".getBytes()), inputStream); } diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java index b8b8bd91..52245943 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java @@ -46,7 +46,7 @@ * When used as a test suite for a concrete implementation, subclass and name the class *Test etc to conform * with Junit rules *

        - * Subclasses should test both V3 KDBX files (Attachment.kdbx) and V4 (Attachment-ChaCha20-Argon2.kdbx) since + * Subclasses should test both V3 KDBX files (Attachment.kdbx) and V4 (V4-ChaCha20-Argon2-Attachment.kdbx) since * attachments are handled differently in the two versions. * * @author jo @@ -135,7 +135,7 @@ public void checkAddChangeRemoveBinaryProperty() { assertTrue(entry.removeBinaryProperty("test")); // false that same property was removed assertFalse(entry.removeBinaryProperty("test")); - // false that non existent was removed + // false that non-existent was removed assertFalse(entry.removeBinaryProperty("test-test")); // same number of properties as we started with assertEquals(1, entry.getBinaryPropertyNames().size()); diff --git a/test/src/main/resources/V4-AES-AES.kdbx b/test/src/main/resources/V4-AES-AES.kdbx old mode 100755 new mode 100644 index 03c60ab45fd5acdf540049b036e09e3a8a718cf6..da60da3b86e0c2440fbf991d81a2197eaa9b116e GIT binary patch literal 2055 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 literal 1966 zcmV;f2T}L~*`k_f`%AR}00RI55CAd3^5(yBLr}h01tDtuTK@wC0096100bZaX+n1V z{B8FWmq*zMVpzYw3$)C?sGm}%$*jH&{D-{)1t0+HxAx8bO0f=T1@+K_vMLV6=UgOE zHX0%+r`C|?A40PR2moN}00000000LN09S*Qlc%b^nTd~Wt>YoOHV7a9(psAXM9v&O zQJ%Mvdkz2_OLFM$3=d>d$ENYNjg)$?eKLT8k=CWX5on z&w4sySh7h91ONg60000401XNa3U4|y0l}7)XE-`c8f!oPWq3;gCa3fSnyu_&T%_?Z z@OD9vUZMk|VHf#`6!O0L2%d_}c<9d7E|7qPunkIF79STPPWnI3sBEPx_qPB7756%# z($Ba!(S>?;FDz|p{<9J9qxhgX|5Tp`j@Uq7R9G-Df!?3a=8yZ_=Kbq4opb>AKOal} zr#k9=)b=MR`G>#&4y3)59j&=xjTgQR2{JCkIuZrB>GZ-S=&?hlwf2iU!_03|vKG-d zK?XpXq6ehyrOZrz>5*Jn@if1ha%AKm(qXREszy|7_-|v#yF!5S{O{Tsf)|5zZl~^04ipa z#bk@3ow->(PKK?{urIJ?v-bd~f(-v|sjbw!nj&6aI~c5dS@j&35*Ft+w7RDHFBln=jIBfzs+cUc@-Hc0zGl&4PK2d_( zTXzuN7VDuaTa*{3$-_^as{o%1C|oZFZf?+7O&^XabwvSe>X~fMN)hIMJnUx+NA%~J zPox7^R5Jpc?i||+7}^qNXis@7fYcUX3P^}p4Y(y^Qono3(i@kV)SK-qMdQm@^S z#VNtqv@N`Z?fLqi%M>?S#jssyjwe-o$l=prsi7Mxu!whyDLyX*zWQGfANhY?-Y~*x zP{WK(%7uhpi}#crP(E&e?JKNjy8)px&;p($>q3?Y?dgSUt83pH%X^G=dpt}1`E+V- z;W&leoIFW<#8856iN{Y}o;370QInAkH|NFe$iGXyDj+kc1BdQWz>ZUW*z>`O@r7v( z8A<=Tjd-ta)-}4#gr^EK3?uT*w)~j|2NIx_Jv-V^oe0*ORE*DFxhNe(r1t^90QFbk zK4D0bbk=)DfX(hOdrN=F%mP>|r>TQ7pW!G%{lQPC=~%<>q_-x5JkuJp?!g|eXb_ zV}$4q2m!oq;EMa3sj4ux$C2!_GBkqT*jdG!qVoy;P;->D+sr@wo?8bgp?BeYLdnQs zN!qwg0kz2!NiMgIf?BOweAgf#&8Ypgi`uP&QVBQf!WK+57QOUz%83kWgl$Pw(Vrvy zc98q`<55QWQ9{{_Ba;ZK^hNsP{6LPCxvstG40EyjEvd1B&Xp(kBOuiXT@Np54|N86 zfr+6j2aZ@4J{x0Nv_;VD1QcHf*nh!;#W=aakt`coEfVggb*e2(+qXhIY$U+>6WpEKVUA6d z0%KZxt9zOzAEua*mnPS>*un0~TW!F#6oy)oK!G>I%+mf3g-ZE=1$_#JK^P-* zD?j(s@&0$pM-+egDf6NN@8JOx2KcFtIf*SjXv>7kY&@a>!Hr-DOYKpULm1i-8q;%s@F5sVyCuDWcmqIOf$ui z|E!*+K<7DGimAqEkd<{3#FDAzIh=3?UM=$sGb*ZW6}8n+a diff --git a/test/src/main/resources/V4-ChaCha-AES.kdbx b/test/src/main/resources/V4-ChaCha-AES.kdbx deleted file mode 100755 index c3e212f8c7157b2d5a9f4f70c55014a76c1533b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1958 zcmV;X2U++7*`k_f`%AR}00RI55CGN#iYtq6OtqyXGnz5mwVDG20096100bZaIT41L zw9M)<^5GX#I=k9RT@XH`?c^+v#MpGp+02P?1t0)SInY>OTy23C86MKJ(Zu!ny5W9M z_01eysFgLO)IiAw2moN}00000000LJ07@oc_tBCQpIa+N&p-WeTYy0;e2_OJ#%TKaL-3O)x8A|P{D_}r4_YOykF#cH=OJUue z=FW5q1ONg60000401XNa3KqCRn3ZIgM6HJl=K zGLrhWNgn9PkN35%T+R!B&JE8$x5awSea7MdHfm;JNVU-@>=(`1k~}tr&pD0NXoH2w znG|8|_R8|#K8<&2s9ujKrfXPj3)~pK#^vxEvXO-P)#{X#X)W0tWfPoabz^GGO7Gie zNY(jjInO*Q5(u;4(vfdc1Kl81Qw!mk(7=wL=}eDBtm7_eW!FxjJ!7v$pipnQ$9Cu` z^5{re(`SN?bK?$_8d31VRfkcww9MwORqh285l60RpSzy7y|OJ@^w1TRZzSv?*kRiH zb({i`Of;cW&qI`er>bjq=4BzJURp4Ri;tV;dHuCS74!iyRgABse-nBxX@B_^1hWJD zhKNoY3kf}ODe!hdphBK>>SAy%!HO<2=_d>25+rbW4)BBig6au-)IIQE#Fz*Xt^wXJ zTyX^O5ssb-S9G?A;Q$D@uM~nTdj6~MSf;QSJmAR5P;Aq$Uduqwq8c0W*VO^|0uOT9 zjuN=Q*$`2Mv=c={xS0?hN{t*Tu3gNb>ER zQU>?83>FzJ?N#{Yno`W-%DXSY2K*nn4jmr+sALO`plh6h@yDpkBu#sX4sJBStX*bQ z(xvmbq7!`{=zuL(aHr}KZr7cs@vP5gR5<(I-xE2@7^K0J5F=n{jhF8sl`6C3?#cwc z6Yg_}F*EpJMvhrqrV`b>JT-A;7Lvd6LOV_1Aq=vvD;xq$>_-AuOOKYe^(5j#o&jqy zZewriYA>-6O7jU2wWuAe#Oncj2oPUcVJ?&G<1>IQXggs{+^N(E&xMqF9hg6h4vi6YlcC>S#aiUMR2Bn^!b&PK~zPNXo*ll0}sbtDFi5aET^-<8!hl`Mu|2nT? zA*fS8ua6Gx-C0P825&LD? zSoQa2dgPJ^mbwBryZnQU-fJoZy73ZP!8biYP;1DKaOUUgBQTqS3)Vzx)q6~-3}%k!pK=C%}; zLuuX8WLYH!?uD%bKT#c6AgsAo__2H_KGALNKyXx?kcCdZ<2O=8xf~Ry$^thz~DF+=dZB31zp3IvJNBhE+B9R+04}zTg)|0QHHs z+x8QGJX*sm<_0oHc;5c$c9b=m2Phy5 z;HIAU`Q@aW0q~CuWO%^!EYcP?fzdwOXu)(dRKV_%Q5D|Q2^ zYoAb@I)OeJbS<&5kORp&03E%T9bl4zIom4@i^rk|z~2!)t^JyzO|ek&5NT2V*krjJ z8tNsgbPX2_>-kt2tNQ20iKj2o`K3=Z;6hJ+vK`y0PqkdaafD|vQ+Hj`wrRb(v8JKo zPjEthvUwKAv@^Vj4_i{9u81GWA3a^Wf|%yVH3%yKrL=$`nMQzAs347H^~~o=CC}Un s0vr8zhsbo+eQwNZwFkpBdSj<1vJaUuP?tE<8D|qD$%?_=H@RKB2gI?uqyPW_ diff --git a/test/src/main/resources/V4-ChaCha20-AES.kdbx b/test/src/main/resources/V4-ChaCha20-AES.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..9f33a1f111518badf9757b20a8d8384f5a529d37 GIT binary patch literal 2039 zcmVt<800BY;0000aRaHqu5C8xG$=UOoVv0myzjO@00000 z002S(0000}AOHXWPB-DhX^ZtLzp(3fRR3jv&RYo}q0Y!`fL<~UOq2y`000C4000dN z4GJtua!^+GpZ6R=J|vj{&J&qDt2AM$q=nnQ(abO`Xe~rp~OC#Y820yauHL zv{QWG@vpt)+%%#nXN3Uc*Ef-W=T_d6amfJI^&((*Q(THw%e{q(Y%fkYysV@K003Z% zwlRql=(TzUTgz3BaY-c``2#IrAahJBwDgU^yw;&I4zedPz=886oEOvxv@vU0xqwJG z#GM#{|NdU|iSx+rEaSfYPbprcCkyyXPTLRZ`@$=sYs%AD|Ml_vtERIGBJu(3=d!oT zy)kTQA@fabI=OJ@@XYAr?A0j(Zy4`Fp2D149O$neb;Wm+I$*Q*Q=8N@VY!8#(${t-{jN`3;Pfx$}R%1u+4M^n!7Utq2hq^Ie0$tXVcux zNj;1rJKxfn&IB*xo&O^K^_>Pc&b=E*Y_C4kBwy}ZpMpK_r4yjbxLij#b@bzz73tsY ztHH#Um8Ivs?AZFG;o1$^aEQ)UXm2e4UVFlP>iBK_54O4H{ zXL4gxey4)RdZm@yr?(ejCCycd53uImC-s<6zADT}?^H%#nuc!C3pvL*n&F6Rydc-C z!Hzm_$LkB%{{LF26R)@vr>}2rYUrS1?;jWO_+R3jF)tTtU-Lv5LnL1@cPQ^zX7I$? zM$|V;CWq73+td}7P>l1tjfk8jv=sJ-HBBaVi~t@N;~0H!SnYlayY?oWEX;aPIW?`B z)wGZYu({4NqgVpmAiQRS2rqNu+Rcd3LDo2fylV!7z5QihYT(j7*@Hyau})EvN>Ig6 zLT!PR4c#wvG7~cjK})a@EV(QzfY*`;L>JbL?&qN+hy06lKAxjlJ>-(g!RS@XPq zpcnF1PYK2MF1%Z^_KRL8Qs{9)2+p4^E5c_OIN{GsqRDiIaUcMHjhW5Y17lFQ%Ri`p zqU!pttKrl07}y?HMR9$5WM?+G2^Yu8gn?^0b`j1whxe4&oISx5Lx}?N&*4MOlwB2# zJ32CRSBVew__4`R$DYfD^| z5SLHruLmy|lIz`8JbXkh2=*P(;QPcw2J!6-cg$xy4e6p z9-O)kd_^YOWI)PU?&sE^Q2snfR9cnn)KG)NBNpd0!?`ri9s1FwYbBY62A`SP9I<5eGR58}~Tj|y_lb~=NyKCG%_S+;NO_Q!>2FaeCX zC;z9}ytu5T1P{<%+UCoWb~tkGHT}y`AX9=>0fkGdZ|Hk%t_ehVLY!$0Ui9M(5f75;s~YW09qBg)^k}3##LN> z!xFyAv4GGBcM_jJQQ7t*72*ecXesRW2G04#ejWxa$juM{EH3^^k@`aZO*ezZ3Tz49 zkPmLk546q04uo{vM@imm^e4*1cpRo-6hqnygW3f!)vlVvH`3?B^{Yat4QKDvS1*)+ z6xTdh5yysxp7A+zKk}p|QY@~*^69syj%W-M)zgGjx*Q78hUz%5M(=2i>}B?_2XE8&3?8#$wD;C)3CzX&IA#6h+LX>raPPGJDqNyBPFLhIySfkVBCDP Vv83wZp~Ixu^I_!5g + + + KeePass + New V4 Database + mNL70Q4AAAA= + This is a new database + mNL70Q4AAAA= + + ddH70Q4AAAA= + 365 + + ddH70Q4AAAA= + -1 + -1 + + False + False + True + False + False + + + True + AAAAAAAAAAAAAAAAAAAAAA== + ddH70Q4AAAA= + AAAAAAAAAAAAAAAAAAAAAA== + ddH70Q4AAAA= + 3mzTUVoVAkO7DFaZ6Soxrw== + 3mzTUVoVAkO7DFaZ6Soxrw== + 10 + 6291456 + vKTp2w4AAAA= + + + + 3mzTUVoVAkO7DFaZ6Soxrw== + NewV4Database + + 49 + + ddH70Q4AAAA= + ddH70Q4AAAA= + tdL70Q4AAAA= + PNH70Q4AAAA= + False + 4 + ddH70Q4AAAA= + + True + + null + null + t53t29Y1A0abjVS1+xTo5g== + + t53t29Y1A0abjVS1+xTo5g== + 0 + + + + + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + + Notes + Notes + + + Password + pWyvoOZreWQ= + + + Title + Sample Entry + + + URL + https://keepass.info/ + + + UserName + User Name + + + True + 0 + + + Target Window + {USERNAME}{TAB}{PASSWORD}{TAB}{ENTER} + + + + + + YeputjepZky3vXID4/HHrA== + 0 + + + + + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + + Notes + + + + Password + BYn/XRc= + + + Title + Sample Entry #2 + + + URL + https://keepass.info/help/kb/testform.html + + + UserName + Michael321 + + + True + 0 + + + *Test Form - KeePass* + + + + + + + TyxuU/7td0iU8+b0iHRh3w== + 0 + + + + + + mNL70Q4AAAA= + OiL90Q4AAAA= + jCL90Q4AAAA= + PNH70Q4AAAA= + False + 1 + OiL90Q4AAAA= + + + Notes + + + + Password + OvGvQqk= + + + Title + Sample Entry #2 - Copy + + + URL + https://keepass.info/help/kb/testform.html + + + UserName + Michael321 + + + True + 0 + + + *Test Form - KeePass* + + + + + + + 3BxuF+oUekuMpLe67xmTLQ== + General + + 48 + + mNL70Q4AAAA= + mNL70Q4AAAA= + tNL70Q4AAAA= + PNH70Q4AAAA= + False + 2 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + vQsKl6dJiEiNvbTkjDVHcw== + Windows + + 38 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + 6bNmbKf/QUKN7I3GlNqqQw== + Network + + 3 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + Qz8rqQvYZUummUrakV6BFQ== + Internet + + 1 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + WMP+6xrurE+Jr6kFDwJtxQ== + eMail + + 19 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + mSY4yshaIU64BNreFBm+1w== + Homebanking + + 37 + + mNL70Q4AAAA= + mNL70Q4AAAA= + s9L70Q4AAAA= + PNH70Q4AAAA= + False + 1 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + + + diff --git a/test/src/main/resources/xml/V4-AES-Argon2.xml b/test/src/main/resources/xml/V4-AES-Argon2.xml new file mode 100644 index 00000000..3ebf3a01 --- /dev/null +++ b/test/src/main/resources/xml/V4-AES-Argon2.xml @@ -0,0 +1,266 @@ + + + + KeePass + mNL70Q4AAAA= + New V4 Database + mNL70Q4AAAA= + This is a new database + mNL70Q4AAAA= + + ddH70Q4AAAA= + 365 + + ddH70Q4AAAA= + -1 + -1 + + False + False + True + False + False + + True + AAAAAAAAAAAAAAAAAAAAAA== + ddH70Q4AAAA= + AAAAAAAAAAAAAAAAAAAAAA== + ddH70Q4AAAA= + 10 + 6291456 + 3mzTUVoVAkO7DFaZ6Soxrw== + 3mzTUVoVAkO7DFaZ6Soxrw== + + + + + 3mzTUVoVAkO7DFaZ6Soxrw== + NewV4Database + + 49 + + ddH70Q4AAAA= + ddH70Q4AAAA= + tdL70Q4AAAA= + PNH70Q4AAAA= + False + 4 + ddH70Q4AAAA= + + True + + null + null + t53t29Y1A0abjVS1+xTo5g== + + t53t29Y1A0abjVS1+xTo5g== + 0 + + + + + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + + Notes + Notes + + + Password + 6f9J8T82DJ0= + + + Title + Sample Entry + + + URL + https://keepass.info/ + + + UserName + User Name + + + True + 0 + + Target Window + {USERNAME}{TAB}{PASSWORD}{TAB}{ENTER} + + + + + + YeputjepZky3vXID4/HHrA== + 0 + + + + + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + + Password + heKymtY= + + + Title + Sample Entry #2 + + + URL + https://keepass.info/help/kb/testform.html + + + UserName + Michael321 + + + True + 0 + + *Test Form - KeePass* + + + + + + + 3BxuF+oUekuMpLe67xmTLQ== + General + + 48 + + mNL70Q4AAAA= + mNL70Q4AAAA= + tNL70Q4AAAA= + PNH70Q4AAAA= + False + 2 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + vQsKl6dJiEiNvbTkjDVHcw== + Windows + + 38 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + 6bNmbKf/QUKN7I3GlNqqQw== + Network + + 3 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + Qz8rqQvYZUummUrakV6BFQ== + Internet + + 1 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + WMP+6xrurE+Jr6kFDwJtxQ== + eMail + + 19 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + mSY4yshaIU64BNreFBm+1w== + Homebanking + + 37 + + mNL70Q4AAAA= + mNL70Q4AAAA= + s9L70Q4AAAA= + PNH70Q4AAAA= + False + 1 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + + + diff --git a/test/src/main/resources/xml/V4-ChaCha20-AES.xml b/test/src/main/resources/xml/V4-ChaCha20-AES.xml new file mode 100644 index 00000000..1b687758 --- /dev/null +++ b/test/src/main/resources/xml/V4-ChaCha20-AES.xml @@ -0,0 +1,319 @@ + + + + KeePass + New V4 Database + mNL70Q4AAAA= + This is a new database + mNL70Q4AAAA= + + ddH70Q4AAAA= + 365 + + ddH70Q4AAAA= + -1 + -1 + + False + False + True + False + False + + + True + AAAAAAAAAAAAAAAAAAAAAA== + ddH70Q4AAAA= + AAAAAAAAAAAAAAAAAAAAAA== + ddH70Q4AAAA= + 3mzTUVoVAkO7DFaZ6Soxrw== + 3mzTUVoVAkO7DFaZ6Soxrw== + 10 + 6291456 + E6Xp2w4AAAA= + + + + 3mzTUVoVAkO7DFaZ6Soxrw== + NewV4Database + + 49 + + ddH70Q4AAAA= + ddH70Q4AAAA= + tdL70Q4AAAA= + PNH70Q4AAAA= + False + 4 + ddH70Q4AAAA= + + True + + null + null + t53t29Y1A0abjVS1+xTo5g== + + t53t29Y1A0abjVS1+xTo5g== + 0 + + + + + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + + Notes + Notes + + + Password + QwJNlVEnjJU= + + + Title + Sample Entry + + + URL + https://keepass.info/ + + + UserName + User Name + + + True + 0 + + + Target Window + {USERNAME}{TAB}{PASSWORD}{TAB}{ENTER} + + + + + + YeputjepZky3vXID4/HHrA== + 0 + + + + + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + + Notes + + + + Password + d0Izuk4= + + + Title + Sample Entry #2 + + + URL + https://keepass.info/help/kb/testform.html + + + UserName + Michael321 + + + True + 0 + + + *Test Form - KeePass* + + + + + + + TyxuU/7td0iU8+b0iHRh3w== + 0 + + + + + + mNL70Q4AAAA= + OiL90Q4AAAA= + jCL90Q4AAAA= + PNH70Q4AAAA= + False + 1 + OiL90Q4AAAA= + + + Notes + + + + Password + cwPX20A= + + + Title + Sample Entry #2 - Copy + + + URL + https://keepass.info/help/kb/testform.html + + + UserName + Michael321 + + + True + 0 + + + *Test Form - KeePass* + + + + + + + 3BxuF+oUekuMpLe67xmTLQ== + General + + 48 + + mNL70Q4AAAA= + mNL70Q4AAAA= + tNL70Q4AAAA= + PNH70Q4AAAA= + False + 2 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + vQsKl6dJiEiNvbTkjDVHcw== + Windows + + 38 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + 6bNmbKf/QUKN7I3GlNqqQw== + Network + + 3 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + Qz8rqQvYZUummUrakV6BFQ== + Internet + + 1 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + WMP+6xrurE+Jr6kFDwJtxQ== + eMail + + 19 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + mSY4yshaIU64BNreFBm+1w== + Homebanking + + 37 + + mNL70Q4AAAA= + mNL70Q4AAAA= + s9L70Q4AAAA= + PNH70Q4AAAA= + False + 1 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + + + diff --git a/test/src/main/resources/xml/V4-ChaCha20-Argon2-Attachment.xml b/test/src/main/resources/xml/V4-ChaCha20-Argon2-Attachment.xml new file mode 100644 index 00000000..1a6574c2 --- /dev/null +++ b/test/src/main/resources/xml/V4-ChaCha20-Argon2-Attachment.xml @@ -0,0 +1,154 @@ + + + + KeePass + BoES0g4AAAA= + + qZ1Tzw4AAAA= + + qZ1Tzw4AAAA= + + qZ1Tzw4AAAA= + 365 + + tp1Tzw4AAAA= + -1 + -1 + + False + False + True + False + False + + True + AAAAAAAAAAAAAAAAAAAAAA== + qZ1Tzw4AAAA= + AAAAAAAAAAAAAAAAAAAAAA== + qZ1Tzw4AAAA= + 10 + 6291456 + TOEWAvT9GlIjOkSMv9F+aw== + TOEWAvT9GlIjOkSMv9F+aw== + + + + + TOEWAvT9GlIjOkSMv9F+aw== + Root + + 48 + + qZ1Tzw4AAAA= + qZ1Tzw4AAAA= + qZ1Tzw4AAAA= + qZ1Tzw4AAAA= + False + 0 + qZ1Tzw4AAAA= + + True + + null + null + GPv+n0B4upj0sZd+nuuiBQ== + + GPv+n0B4upj0sZd+nuuiBQ== + 0 + + + + + + wZ1Tzw4AAAA= + J55Tzw4AAAA= + J55Tzw4AAAA= + wZ1Tzw4AAAA= + False + 0 + J55Tzw4AAAA= + + + Notes + + + + Password + + + + Title + Test attachment + + + URL + + + + UserName + + + + letter J.jpeg + + + + True + 0 + + + + + mwq3nFNpPrvjXlsWy/6FTQ== + 0 + + + + + + 2KJTzw4AAAA= + DKNTzw4AAAA= + DKNTzw4AAAA= + 2KJTzw4AAAA= + False + 0 + DKNTzw4AAAA= + + + Notes + + + + Password + + + + Title + Test 2 Attachments + + + URL + + + + UserName + + + + letter J.jpeg + + + + letter L.jpeg + + + + True + 0 + + + + + + + \ No newline at end of file From 2eb4ef653f1d1b373be6a1bfd7b4a05a2e68827e Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 8 May 2023 13:46:46 +0100 Subject: [PATCH 086/228] Make SecureRandom static and fix for slow random generation #12 and implicitly make random generation configurable --- .../org/linguafranca/pwdb/kdbx/KdbxHeader.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java index e7adc544..ca7956b5 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java @@ -25,6 +25,7 @@ import java.io.OutputStream; import java.nio.ByteBuffer; import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; @@ -99,7 +100,18 @@ public enum CompressionFlags { /* the bytes that compose the outer header, required for V4 to calculate the HMac */ private byte[] headerBytes; - final SecureRandom random; + // Make static and try to use SHA1PRNG per + // https://stackoverflow.com/questions/137212/how-to-deal-with-a-slow-securerandom-generator + // and comment on issue #12. If you don't like this then you can of course set this + // to something else + public static SecureRandom random; + static { + try { + random = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + random = new SecureRandom(); + } + } /** * Provides for choice of version number and crypto options for V3 and V4 */ @@ -169,8 +181,6 @@ public KdbxHeader(int version) { public KdbxHeader(KdbxHeaderOptions opts) { - random = new SecureRandom(); - this.version = opts.getVersion(); setCipherAlgorithm(opts.getCipherAlgorithm()); setKeyDerivationFunction(opts.getKeyDerivationFunction()); From 45d9eed5b4ce95f23b6a6282eb9fff2d52f76805 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 8 May 2023 15:04:18 +0100 Subject: [PATCH 087/228] Fix missing JAXB dependencies on Java 11 and later #31 --- jaxb/pom.xml | 11 +++++++++++ readme.md | 8 +++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/jaxb/pom.xml b/jaxb/pom.xml index cbf74713..11be743d 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -34,6 +34,17 @@ KeePassJava2-kdbx ${project.version} + + javax.xml.bind + jaxb-api + 2.3.1 + + + com.sun.xml.bind + jaxb-impl + 2.3.3 + runtime + org.linguafranca.pwdb test diff --git a/readme.md b/readme.md index 3e56bc5f..97ca3e89 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,9 @@ [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent) A Java 8 API for databases compatible with the renowned [KeePass](http://keepass.info) password -safe for Windows. +safe for Windows. This is a "headless" implementation - if you want something with a UI +then [KeePassXC](https://keepassxc.org/) and [KeePassDX](https://www.keepassdx.com/) could +be just the things for you. Features to date: @@ -161,6 +163,10 @@ The Simple XML implementation additionally depends on: - [Simple XML Serialisation Framework](http://simple.sourceforge.net/) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0)). - [Faster XML Aalto](https://github.com/FasterXML/aalto-xml) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0.txt)). +On Java 11 and later Jaxb implementation depends on: + +- [JAXB](https://javaee.github.io/jaxb-v2/) + It also depends on SLF4J and Junit for tests. ## Build from Source From ba580336ad3e3a8e7aa78fe3fffa8df897748e23 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 8 May 2023 18:18:17 +0100 Subject: [PATCH 088/228] Loosen up on the date formats allowed - in this enhancement request we have TZD of 00:00 (rather than Z) and 6 digits of fractional seconds #27 --- .../pwdb/validation/Issue_27_Test.java | 62 ++++++++++++++++++ .../pwdb/validation/Issue_38_Test.java | 10 +-- .../org/linguafranca/pwdb/kdbx/Helpers.java | 29 ++++---- .../resources/issue-27/bogus-timestamp.kdbx | Bin 0 -> 1278 bytes .../resources/issue-27/bogus-timestamp2.kdbx | Bin 0 -> 1278 bytes 5 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 example/src/test/java/org/linguafranca/pwdb/validation/Issue_27_Test.java rename dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/Issue38Test.java => example/src/test/java/org/linguafranca/pwdb/validation/Issue_38_Test.java (61%) create mode 100644 test/src/main/resources/issue-27/bogus-timestamp.kdbx create mode 100644 test/src/main/resources/issue-27/bogus-timestamp2.kdbx diff --git a/example/src/test/java/org/linguafranca/pwdb/validation/Issue_27_Test.java b/example/src/test/java/org/linguafranca/pwdb/validation/Issue_27_Test.java new file mode 100644 index 00000000..da945292 --- /dev/null +++ b/example/src/test/java/org/linguafranca/pwdb/validation/Issue_27_Test.java @@ -0,0 +1,62 @@ +package org.linguafranca.pwdb.validation; + +import com.google.common.io.CharStreams; +import org.junit.Ignore; +import org.junit.Test; +import org.linguafranca.pwdb.kdbx.Helpers; +import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.KdbxHeader; +import org.linguafranca.pwdb.kdbx.KdbxSerializer; +import org.linguafranca.pwdb.kdbx.jaxb.JaxbDatabase; +import org.linguafranca.pwdb.kdbx.jaxb.JaxbEntry; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class Issue_27_Test { + /** + * Check load of problem file + */ + @Test + public void testIssue27() { + InputStream is = this.getClass().getClassLoader().getResourceAsStream("issue-27/bogus-timestamp2.kdbx"); + KdbxCreds creds = new KdbxCreds("passwordless".getBytes()); + JaxbDatabase db = JaxbDatabase.load(creds, is); + List entries = db.findEntries("testtitle"); + + for (JaxbEntry entry: entries) { + System.out.println(Helpers.fromDate(entry.getCreationTime())); + assertEquals("2021-01-11T09:18:56Z", Helpers.fromDate(entry.getCreationTime())); + } + } + + /** + * Verify that V4 dates are still processed correctly + */ + @Test + public void testV4Date() { + InputStream is = this.getClass().getClassLoader().getResourceAsStream("V4-AES-AES.kdbx"); + KdbxCreds creds = new KdbxCreds("123".getBytes()); + JaxbDatabase db = JaxbDatabase.load(creds, is); + List entries = db.findEntries("Sample Entry #2 - Copy"); + + for (JaxbEntry entry: entries) { + System.out.println(Helpers.fromDate(entry.getCreationTime())); + assertEquals("2018-01-26T13:20:58Z", Helpers.fromDate(entry.getCreationTime())); + } + } + + @Test @Ignore + public void testIssue27XML() throws IOException { + + InputStream is = this.getClass().getClassLoader().getResourceAsStream("issue-27/bogus-timestamp2.kdbx"); + KdbxCreds creds = new KdbxCreds("passwordless".getBytes()); + InputStream plainText = KdbxSerializer.createUnencryptedInputStream(creds,new KdbxHeader(), is); + System.out.println(CharStreams.toString(new InputStreamReader(plainText, StandardCharsets.UTF_8))); + } +} diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/Issue38Test.java b/example/src/test/java/org/linguafranca/pwdb/validation/Issue_38_Test.java similarity index 61% rename from dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/Issue38Test.java rename to example/src/test/java/org/linguafranca/pwdb/validation/Issue_38_Test.java index a40568d0..dd256347 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/Issue38Test.java +++ b/example/src/test/java/org/linguafranca/pwdb/validation/Issue_38_Test.java @@ -1,7 +1,9 @@ -package org.linguafranca.pwdb.kdbx.dom; +package org.linguafranca.pwdb.validation; import org.junit.Test; import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.dom.DomDatabaseWrapper; +import org.linguafranca.pwdb.kdbx.dom.DomEntryWrapper; import java.io.IOException; import java.io.InputStream; @@ -10,12 +12,12 @@ /** * Bug report on GitHub, the Keyfile is Version 2 (Hex) */ -public class Issue38Test { +public class Issue_38_Test { @Test public void testV2Keyfile() throws IOException { - InputStream databaseStream = Issue38Test.class.getClassLoader().getResourceAsStream("issue-38/Database/Database.kdbx"); - InputStream keyStream = Issue38Test.class.getClassLoader().getResourceAsStream("issue-38/Database/Database.keyx"); + InputStream databaseStream = Issue_38_Test.class.getClassLoader().getResourceAsStream("issue-38/Database/Database.kdbx"); + InputStream keyStream = Issue_38_Test.class.getClassLoader().getResourceAsStream("issue-38/Database/Database.keyx"); assert keyStream != null; KdbxCreds creds = new KdbxCreds("MyPassword".getBytes(), keyStream); assert databaseStream != null; diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java index ef6497bd..ff89a587 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java @@ -27,6 +27,10 @@ import java.nio.ByteOrder; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Date; import java.util.UUID; import java.util.zip.GZIPInputStream; @@ -75,24 +79,21 @@ public static String fromBoolean(Boolean value) { return value == null ? "False" : (value ? "True" : "False"); } - private static SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"); - - private static Date baseDate; - - static { - try { - baseDate = inFormat.parse("0001-01-01T00:00:00Z"); - } catch (ParseException ignore) { - // hmm, cannot happen - } - } + public static final SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); + public static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ"); + public static final Date baseDate = Date.from(ZonedDateTime.parse("0001-01-01T00:00:00Z").toInstant()); // in V3 this is just a date, in V4 it's a base64 encoded serial number of seconds after the base date above public static Date toDate(String value) { try { - return inFormat.parse(value); - } catch (ParseException ignored) {} - // V4 + return Date.from(ZonedDateTime.parse(value).toInstant()); + } catch (DateTimeParseException e) { + // check if it may be a base64 encoded value ending in = + if (!value.endsWith("=")) { + throw new IllegalArgumentException(value + " is not a valid date"); + } + } + // V4 dates are base 64 encoded seconds since baseDate byte [] b = decodeBase64Content(value.getBytes()); long secondsSinceBaseDate = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getLong(); return new Date(secondsSinceBaseDate * 1000 + baseDate.getTime()); diff --git a/test/src/main/resources/issue-27/bogus-timestamp.kdbx b/test/src/main/resources/issue-27/bogus-timestamp.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..cdf8ad6aba643b7a504f0e28beb8c681e658ccc1 GIT binary patch literal 1278 zcmV!j(P)dCXh2)kf;pDH8zW_ty!1t0*Y+d1I@u21PAsno#N!wlq2mqjl0RR91000LN0IF1O5Cmb3htB}J#>4gUo(LcSPkZDwYO8>X zVdxt(uew=`xn8QTZhG%2`v9<0c3)!G2_OLS5Q*+KJBLB%%To%e4_o)sf13HYw`G|< zW+HHz51a-H1ONg60000401XNa3b;$E<@Xw5#32=W9{qOm-dZHx#y1OO$Gim&GpbJ3 z=0W_?WHBJLV2c7?mO|EU4_SBJdS$!t#9h}QDqJodOSvs5#cRVVJ}KIXVy$^?Wp}n_ z-U-+iq|WoGF12X^PNW5$u?IY|#x5fYSUq#{DemfWbk+|Q!xw&``{=C+m zXprZw9#<8G@0^3)bvsE+55>B{rCw*SK3_Z{^AEh!SDWXuo0!TJy}8~rVTZf*CcULd zgGr&=e=Y6Bmr3USG5FHPF(c2MzW1T!O3Wu zprc6(YX^v>q^VE62F?u%!Os}N-DT@az-V9GGS>sFM?7`!R2=NjRZUD%!FUaR5G| zwZyCnynJZtL|MJFo|0vS9g_qg0?b*{#kdbzJ`bfVyUuzO#Km#=)8=}btV6yl&)oz= z!6A%%i+TSMS#kwfQK{GKGD6m>aVM7D-^%qMuL^fv9`ubq;VXCfMp9jt@91h8e%V2E zq$mtF4O2~J4Icww>+0F-YOMdb&I_b8kQyuH@Jg%s0Fi$DC|;@Qk_f+QV`~-IlF8lu zn#%D1Z8es6%l|E}hwpWC=w0YpAo-KcHeU_%rL{_|6)tNt(0)_3iV8NPxAwgAgp~>g zcSghnes_kGz}8XXAHWV8?KQXG#49|PZMzaCZJ95Zy(tGwQYoGfDF`}kQW+P4W=juF z@*g+5I~!&H+gLoGMg)B5K@-mJN1!SbYH-!>qNS#1ud!!4@?e9wfKqLkZq-#3^H6N) zkH=%zh=WjJggiw8VFAQa`!MXF5R8T(SG*!c34obJwACRS{4hRV35gG>xBm`6`xi_Z z(PY1cT!H6_C8uV`B^pd7Pjm6_+K>4%(k_S1=~S7QNjz|zUnJ!?Ih+F1!Z4x`SeN4| z+yXpY18Sga+vDFRw|-U9Z%P@kxq?o-X-SowOY6on@dwdHfK$i+PT=Nz7%30dVv^w&Vj{`t!IG5 zDmaflBBa7+4mM$O8Xi92!dp*wKgW27SYI`u+RB85xZJMf;b1W7t3YTJ02dsd^;nZf o?#`*P6v2mG4)^T{H{##ieL52@a{*a^Np(mFgW5uL{}PfzFNbkow*UYD literal 0 HcmV?d00001 diff --git a/test/src/main/resources/issue-27/bogus-timestamp2.kdbx b/test/src/main/resources/issue-27/bogus-timestamp2.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..cdf8ad6aba643b7a504f0e28beb8c681e658ccc1 GIT binary patch literal 1278 zcmV!j(P)dCXh2)kf;pDH8zW_ty!1t0*Y+d1I@u21PAsno#N!wlq2mqjl0RR91000LN0IF1O5Cmb3htB}J#>4gUo(LcSPkZDwYO8>X zVdxt(uew=`xn8QTZhG%2`v9<0c3)!G2_OLS5Q*+KJBLB%%To%e4_o)sf13HYw`G|< zW+HHz51a-H1ONg60000401XNa3b;$E<@Xw5#32=W9{qOm-dZHx#y1OO$Gim&GpbJ3 z=0W_?WHBJLV2c7?mO|EU4_SBJdS$!t#9h}QDqJodOSvs5#cRVVJ}KIXVy$^?Wp}n_ z-U-+iq|WoGF12X^PNW5$u?IY|#x5fYSUq#{DemfWbk+|Q!xw&``{=C+m zXprZw9#<8G@0^3)bvsE+55>B{rCw*SK3_Z{^AEh!SDWXuo0!TJy}8~rVTZf*CcULd zgGr&=e=Y6Bmr3USG5FHPF(c2MzW1T!O3Wu zprc6(YX^v>q^VE62F?u%!Os}N-DT@az-V9GGS>sFM?7`!R2=NjRZUD%!FUaR5G| zwZyCnynJZtL|MJFo|0vS9g_qg0?b*{#kdbzJ`bfVyUuzO#Km#=)8=}btV6yl&)oz= z!6A%%i+TSMS#kwfQK{GKGD6m>aVM7D-^%qMuL^fv9`ubq;VXCfMp9jt@91h8e%V2E zq$mtF4O2~J4Icww>+0F-YOMdb&I_b8kQyuH@Jg%s0Fi$DC|;@Qk_f+QV`~-IlF8lu zn#%D1Z8es6%l|E}hwpWC=w0YpAo-KcHeU_%rL{_|6)tNt(0)_3iV8NPxAwgAgp~>g zcSghnes_kGz}8XXAHWV8?KQXG#49|PZMzaCZJ95Zy(tGwQYoGfDF`}kQW+P4W=juF z@*g+5I~!&H+gLoGMg)B5K@-mJN1!SbYH-!>qNS#1ud!!4@?e9wfKqLkZq-#3^H6N) zkH=%zh=WjJggiw8VFAQa`!MXF5R8T(SG*!c34obJwACRS{4hRV35gG>xBm`6`xi_Z z(PY1cT!H6_C8uV`B^pd7Pjm6_+K>4%(k_S1=~S7QNjz|zUnJ!?Ih+F1!Z4x`SeN4| z+yXpY18Sga+vDFRw|-U9Z%P@kxq?o-X-SowOY6on@dwdHfK$i+PT=Nz7%30dVv^w&Vj{`t!IG5 zDmaflBBa7+4mM$O8Xi92!dp*wKgW27SYI`u+RB85xZJMf;b1W7t3YTJ02dsd^;nZf o?#`*P6v2mG4)^T{H{##ieL52@a{*a^Np(mFgW5uL{}PfzFNbkow*UYD literal 0 HcmV?d00001 From 04b1f38c76d8a81901d94dc8252f76faa07551b4 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 9 May 2023 15:50:08 +0100 Subject: [PATCH 089/228] Testing attachments for Version 4 - refs #13 #39 --- .../linguafranca/pwdb/kdbx/dom/DomHelper.java | 2 +- .../kdbx/dom/DomSerializableDatabase.java | 7 ++---- .../kdbx/dom/DomBinaryPropertyV3Test.java | 10 ++++++++ .../kdbx/dom/DomBinaryPropertyV4Test.java | 18 +++++++++++++- .../kdbx/jaxb/JaxbBinaryPropertyV3Test.java | 12 ++++++++++ .../kdbx/jaxb/JaxbBinaryPropertyV4Test.java | 16 ++++++++++++- .../pwdb/kdbx/simple/SimpleDatabase.java | 12 +++++++--- .../simple/SimpleBinaryPropertyV3Test.java | 15 ++++++++++++ .../simple/SimpleBinaryPropertyV4Test.java | 15 ++++++++++++ .../pwdb/checks/BinaryPropertyChecks.java | 24 ++++++++++++++----- 10 files changed, 114 insertions(+), 17 deletions(-) diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java index c09062cc..268f4cf1 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java @@ -165,8 +165,8 @@ static String ensureElementContent(String elementPath, Element parentElement, @N Element result = getElement(elementPath, parentElement, false); if (result == null) { result = createHierarchically(elementPath, parentElement); - result.setTextContent(value); } + result.setTextContent(value); return result.getTextContent(); } diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java index c5f1e6a8..5663cae4 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java @@ -197,11 +197,8 @@ public byte[] getHeaderHash() { public void setHeaderHash(byte[] hash) { // Android compatibility String base64String = new String(Base64.encodeBase64(hash)); - try { - ((Element) DomHelper.xpath.evaluate("//HeaderHash", doc, XPathConstants.NODE)).setTextContent(base64String); - } catch (XPathExpressionException e) { - throw new IllegalStateException("Can't set header hash", e); - } + DomHelper.ensureElementContent("Meta/HeaderHash", doc.getDocumentElement(), base64String); + //((Element) DomHelper.xpath.evaluate("//HeaderHash", doc, XPathConstants.NODE)).setTextContent(base64String); } diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV3Test.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV3Test.java index 7c4b8788..bf85d624 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV3Test.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV3Test.java @@ -39,4 +39,14 @@ public DomBinaryPropertyV3Test() throws IOException { public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { database.save(credentials, outputStream); } + + @Override + public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + return DomDatabaseWrapper.load(credentials, inputStream); + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } } \ No newline at end of file diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV4Test.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV4Test.java index 422ffa24..35e2dafd 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV4Test.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV4Test.java @@ -18,8 +18,11 @@ import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.checks.BinaryPropertyChecks; import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.KdbxHeader; +import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import java.io.IOException; import java.io.InputStream; @@ -37,6 +40,19 @@ public DomBinaryPropertyV4Test() throws IOException { @Override public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { - database.save(credentials, outputStream); + StreamFormat sf = new KdbxStreamFormat(new KdbxHeader(4)); + database.save(sf, credentials, outputStream); } + + + @Override + public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + return DomDatabaseWrapper.load(credentials, inputStream); + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } + } \ No newline at end of file diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV3Test.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV3Test.java index dd87d4c4..d7591bb3 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV3Test.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV3Test.java @@ -39,4 +39,16 @@ public JaxbBinaryPropertyV3Test() { public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { database.save(credentials,outputStream); } + + + @Override + public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + return JaxbDatabase.load(credentials, inputStream); + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } + } diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.java index 97e96dbb..c2f6755f 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.java @@ -19,8 +19,11 @@ import org.junit.Ignore; import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.checks.BinaryPropertyChecks; import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.KdbxHeader; +import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import java.io.IOException; import java.io.InputStream; @@ -39,6 +42,17 @@ public JaxbBinaryPropertyV4Test() throws IOException { @Override public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { - database.save(credentials, outputStream); + StreamFormat sf = new KdbxStreamFormat(new KdbxHeader(4)); + database.save(sf, credentials, outputStream); + } + + @Override + public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + return JaxbDatabase.load(credentials, inputStream); + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); } } \ No newline at end of file diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java index be71e6f4..1cc047d3 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java @@ -202,7 +202,7 @@ public static SimpleDatabase load(Credentials credentials, InputStream inputStre if (kdbxHeader.getVersion() == 4) { int index = 0; for (byte[] binary : kdbxHeader.getBinaries()) { - addBinary(result, binary, index); + addBinary(result, Arrays.copyOfRange(binary, 1, binary.length), index); index++; } } @@ -252,8 +252,14 @@ public void save(Credentials credentials, OutputStream outputStream) throws IOEx KdbxHeader kdbxHeader = new KdbxHeader(); OutputStream kdbxInnerStream = KdbxSerializer.createEncryptedOutputStream(credentials, kdbxHeader, outputStream); - // the database contains the hash of the headers - keePassFile.meta.headerHash.setContent(kdbxHeader.getHeaderHash()); + if (kdbxHeader.getVersion() == 3) { + // the database contains the hash of the headers + if (Objects.isNull(keePassFile.meta.headerHash)){ + keePassFile.meta.headerHash = new KeePassFile.ByteArray(kdbxHeader.getHeaderHash()); + } else { + keePassFile.meta.headerHash.setContent(kdbxHeader.getHeaderHash()); + } + } // encrypt the fields in the XML inner stream XmlOutputStreamFilter plainTextOutputStream = new XmlOutputStreamFilter(kdbxInnerStream, diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV3Test.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV3Test.java index 71ca5944..69d78695 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV3Test.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV3Test.java @@ -43,4 +43,19 @@ public SimpleBinaryPropertyV3Test() { public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { database.save(credentials, outputStream); } + + + @Override + public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + try { + return SimpleDatabase.load(credentials, inputStream); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } } diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java index 3ac501d9..c9b97cc8 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java @@ -41,4 +41,19 @@ public SimpleBinaryPropertyV4Test() throws Exception { public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { database.save(credentials, outputStream); } + + @Override + public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + try { + return SimpleDatabase.load(credentials, inputStream); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } + } \ No newline at end of file diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java index 52245943..b80acc29 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java @@ -26,9 +26,10 @@ import org.linguafranca.pwdb.Entry; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; import static org.junit.Assert.*; @@ -57,6 +58,8 @@ public abstract class BinaryPropertyChecks { @SuppressWarnings("unused") public abstract void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException; + public abstract Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException; + public abstract Credentials getCreds(byte[] creds); /** * Retrieve and verify attachment "letter J" @@ -144,9 +147,18 @@ public void checkAddChangeRemoveBinaryProperty() { /** * Checks that a database with binary properties saves and reloads correctly */ - @Test @Ignore - public void saveAndReloadCheck() { - // TODO + @Test + public void saveAndReloadCheck() throws IOException { + Path file = Files.createTempFile("keepass", "tmp"); + saveDatabase(database, getCreds("123".getBytes()), Files.newOutputStream(file)); + + Database db = loadDatabase(getCreds("123".getBytes()),Files.newInputStream(file)); + Entry entry = (Entry) db.findEntries("Test attachment").get(0); + assertArrayEquals(new String[] {"letter J.jpeg"}, entry.getBinaryPropertyNames().toArray()); + + entry = (Entry) db.findEntries("Test 2 attachment").get(0); + assertArrayEquals(new String[] {"letter J.jpeg", "letter L.jpeg"}, entry.getBinaryPropertyNames().toArray()); + } From 483a1b9f2d4e01df3fb4e7740bfd4bbd2cad5e7c Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 9 May 2023 15:58:00 +0100 Subject: [PATCH 090/228] Documentation tweak --- readme.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/readme.md b/readme.md index 97ca3e89..a40299a1 100644 --- a/readme.md +++ b/readme.md @@ -34,7 +34,7 @@ It is licensed under the Apache 2 License and is currently usable. After a number of years of neglect, the project is (May 2023) back in development. -The current code is version 3.2-SNAPSHOT. This is on the main branch. It has +The current code is version 2.2-SNAPSHOT. This is on the main branch. It has not yet been distributed to Maven. See [Build from Source](#build-from-source) Key updates: @@ -46,7 +46,7 @@ See the [changelog](CHANGELOG.md) for more details. ## Maven Coordinates -The composite POM for the last release (3.1), Java 7 compatible, is +The composite POM for the last release (2.1), Java 7 compatible, is org.linguafranca.pwdb KeePassJava2 @@ -61,7 +61,7 @@ The composite POM for the last release (3.1), Java 7 compatible, is ## Java Version -From release 3.2 it requires Java 1.8. Earlier versions require Java 1.7. +From release 2.2 it requires Java 1.8. Earlier versions require Java 1.7. ## Quick Start @@ -138,7 +138,11 @@ exactly. KeePass is in effect defined by the code that Dominik writes to create and maintain the project. Hence, there is not much by way of definitive specification of KeePass files other than that code. There is a discussion of the [differences between KDBX version 3.1 and version 4](https://keepass.info/help/kb/kdbx_4.html). -There is also a discussion of the [enhancements in KDBX 4.1](https://keepass.info/help/kb/kdbx_4.1.html). +There is also a discussion of the [enhancements in KDBX 4.1](https://keepass.info/help/kb/kdbx_4.1.html), as well +as a discussion of [Key Files](https://keepass.info/help/base/keys.html#keyfiles). + +Massive credit also to the folks over at [KeePassXC](https://keepassxc.org/) who wrote some +[documentation](https://github.com/keepassxreboot/keepassxc-specs) about their understanding of various format things. For the sake of clarification and my own satisfaction I have written about my understanding of @@ -151,12 +155,11 @@ KeePass formats in the following locations: ## Dependencies -Aside from the JRE the API depends on: +Aside from the JRE, at release 3.2, the API depends on: - [Google Guava](https://github.com/google/guava/wiki) ([Apache 2 license](https://github.com/google/guava/blob/master/COPYING)). - [Apache Commons Codec](https://commons.apache.org/proper/commons-codec/) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0)). -- [Spongy Castle](https://rtyley.github.io/spongycastle/) ([License](https://github.com/rtyley/spongycastle/blob/spongy-master/LICENSE.html)) which is a repackaging for Android of [Bouncy Castle](https://www.bouncycastle.org/java.html) ([License](https://www.bouncycastle.org/licence.html)). -- [Jargon2](https://github.com/kosprov/jargon2-api) for KDBX format version 4 Argon2 Key Derivation Function (Apache2 license) +- [Bouncy Castle](https://github.com/bcgit/bc-java/blob/master/LICENSE.html) ([MIT License](https://github.com/bcgit/bc-java/blob/master/LICENSE.html)). The Simple XML implementation additionally depends on: From e0f1e6b9851fa395026f6af366be218742930e7c Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 9 May 2023 15:58:29 +0100 Subject: [PATCH 091/228] Documentation tweak #2 --- readme.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index a40299a1..a88f2f89 100644 --- a/readme.md +++ b/readme.md @@ -151,11 +151,13 @@ KeePass formats in the following locations: 1. The Javadoc header to [KdbxSerializer](http://javadoc.io/page/org.linguafranca.pwdb/KeePassJava2-kdbx/latest/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.html) describes KDBX stream formatting. 2. The XSD Schema [KDBX.4.xsd](KDBX.4.xsd) documents my understanding of the Keepass XML, and also my lack of understanding, in parts. 3. The following graphic illustrates KDBX 3.1 and 4 file formats: + + [![KDBX Formats](KdbxDiagram.svg "KDBX Formats")](KdbxDiagram.svg) ## Dependencies -Aside from the JRE, at release 3.2, the API depends on: +Aside from the JRE, at release 2.2, the API depends on: - [Google Guava](https://github.com/google/guava/wiki) ([Apache 2 license](https://github.com/google/guava/blob/master/COPYING)). - [Apache Commons Codec](https://commons.apache.org/proper/commons-codec/) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0)). @@ -166,7 +168,7 @@ The Simple XML implementation additionally depends on: - [Simple XML Serialisation Framework](http://simple.sourceforge.net/) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0)). - [Faster XML Aalto](https://github.com/FasterXML/aalto-xml) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0.txt)). -On Java 11 and later Jaxb implementation depends on: +For Java 11 and later Jaxb implementation depends on explicit inclusion [no longer provided by JDK](https://docs.oracle.com/en/java/javase/11/migrate/index.html#JSMIG-GUID-F640FA9D-FB66-4D85-AD2B-D931174C09A3) of: - [JAXB](https://javaee.github.io/jaxb-v2/) From d0796d437fd382acd13dac464e1ce558c0ad84fb Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Wed, 10 May 2023 17:50:53 +0100 Subject: [PATCH 092/228] Save and load attachments, improvement to testing, minor tweaks, examples #36, #39, #13 --- .../kdbx/dom/DomBinaryPropertyV3Test.java | 5 + .../kdbx/dom/DomBinaryPropertyV4Test.java | 5 + .../linguafranca/pwdb/kdbx/QuickStart.java | 104 ++++++------------ .../org/linguafranca/pwdb/kdbx/SaxParse.java | 10 +- .../java/org/linguafranca/pwdb/kdbx/Util.java | 61 ++++++++++ .../pwdb/example/SimpleQuickStartTest.java | 81 -------------- .../pwdb/{example => kdbx}/SaxParseTest.java | 11 +- .../pwdb/kdbx/SimpleQuickStartTest.java | 100 +++++++++++++++++ .../org/linguafranca/pwdb/kdbx/UtilTest.java | 33 ++++++ .../{ => kdbx}/validation/Issue_27_Test.java | 2 +- .../{ => kdbx}/validation/Issue_38_Test.java | 2 +- .../kdbx/jaxb/JaxbBinaryPropertyV3Test.java | 4 + .../kdbx/jaxb/JaxbBinaryPropertyV4Test.java | 5 + .../linguafranca/pwdb/kdbx/KdbxHeader.java | 14 +-- .../pwdb/kdbx/KdbxStreamFormat.java | 1 + pom.xml | 5 - .../simple/SimpleBinaryPropertyV4Test.java | 5 + .../pwdb/checks/BinaryPropertyChecks.java | 23 ++++ .../java/org/linguafranca/util/HexViewer.java | 32 ++++++ .../resources/V4-AES-Argon2-CustomIcon.kdbx | Bin 0 -> 26757 bytes 20 files changed, 326 insertions(+), 177 deletions(-) create mode 100644 example/src/main/java/org/linguafranca/pwdb/kdbx/Util.java delete mode 100644 example/src/test/java/org/linguafranca/pwdb/example/SimpleQuickStartTest.java rename example/src/test/java/org/linguafranca/pwdb/{example => kdbx}/SaxParseTest.java (68%) create mode 100644 example/src/test/java/org/linguafranca/pwdb/kdbx/SimpleQuickStartTest.java create mode 100644 example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java rename example/src/test/java/org/linguafranca/pwdb/{ => kdbx}/validation/Issue_27_Test.java (98%) rename example/src/test/java/org/linguafranca/pwdb/{ => kdbx}/validation/Issue_38_Test.java (95%) create mode 100644 test/src/main/java/org/linguafranca/util/HexViewer.java create mode 100644 test/src/main/resources/V4-AES-Argon2-CustomIcon.kdbx diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV3Test.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV3Test.java index bf85d624..58ccdebc 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV3Test.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV3Test.java @@ -45,6 +45,11 @@ public Database loadDatabase(Credentials credentials, InputStream inputStream) t return DomDatabaseWrapper.load(credentials, inputStream); } + @Override + public Database newDatabase() { + return new DomDatabaseWrapper(); + } + @Override public Credentials getCreds(byte[] creds) { return new KdbxCreds(creds); diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV4Test.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV4Test.java index 35e2dafd..97499e18 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV4Test.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV4Test.java @@ -50,6 +50,11 @@ public Database loadDatabase(Credentials credentials, InputStream inputStream) t return DomDatabaseWrapper.load(credentials, inputStream); } + @Override + public Database newDatabase() { + return new DomDatabaseWrapper(); + } + @Override public Credentials getCreds(byte[] creds) { return new KdbxCreds(creds); diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java index 81496d43..6aceb311 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java @@ -21,9 +21,7 @@ import org.linguafranca.pwdb.kdb.KdbCredentials; import org.linguafranca.pwdb.kdb.KdbDatabase; import org.linguafranca.pwdb.kdbx.dom.DomDatabaseWrapper; -import org.linguafranca.pwdb.security.Aes; import org.linguafranca.pwdb.security.Encryption; -import org.linguafranca.pwdb.security.KeyDerivationFunction; import java.io.*; import java.nio.file.Files; @@ -144,99 +142,61 @@ public void loadKdb() throws IOException { } /** - * Load KDBX V3 save as KDBX V4 - then load again and save with different configuration + * Load KDBX V3 save as KDBX V4 */ - public void loadKdbx3SaveKdbx4() throws IOException { + public void loadKdbx3SaveKdbx4(String resourceName, byte[] password, OutputStream v4OutputStream) throws IOException { DomDatabaseWrapper database; // password credentials - KdbxCreds credentials = new KdbxCreds("123".getBytes()); - - try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test123.kdbx")) { + KdbxCreds credentials = new KdbxCreds(password); + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resourceName)) { // load KdbDatabase database = DomDatabaseWrapper.load(credentials, inputStream); } - // visit all groups and entries and list them to console - database.visit(new Visitor.Print()); // create a KDBX (database D kdbxDatabase = getDatabase(); kdbxDatabase.setName("New Database"); - kdbxDatabase.setDescription("Migration of KDB Database to KDBX Database"); + kdbxDatabase.setDescription("Migration of KDBX 3 Database to KDBX 4 Database"); // deep copy from group (not including source group, KDB database has simulated root) kdbxDatabase.getRootGroup().copy(database.getRootGroup()); // choose a stream format - V4 Kdbx and choose some algorithms - KdbxStreamFormat formatV4 = new KdbxStreamFormat(new KdbxHeader(KdbxHeader.KdbxHeaderOpts.V4_AES_ARGON_CHA_CHA)); - KdbxHeader kdbxHeader = formatV4.getStreamConfiguration(); - // change algos from those originally selected + KdbxHeader kdbxHeader = new KdbxHeader(KdbxHeader.KdbxHeaderOpts.V4_AES_ARGON_CHA_CHA); + KdbxStreamFormat formatV4 = new KdbxStreamFormat(kdbxHeader); + // change algorithm from those originally selected kdbxHeader.setCipherAlgorithm(Encryption.Cipher.CHA_CHA_20); - kdbxHeader.setKeyDerivationFunction(Encryption.Kdf.AES); + kdbxHeader.setKeyDerivationFunction(Encryption.KeyDerivationFunction.ARGON2); kdbxHeader.setProtectedStreamAlgorithm(Encryption.ProtectedStreamAlgorithm.CHA_CHA_20); // save it with format options - try (FileOutputStream f = new FileOutputStream("testOutput/CHACHA-AES-CHACHA.kdbx")) { - kdbxDatabase.save(formatV4, credentials, f); - } - - // doesn't matter what we create it will be overwritten - KdbxStreamFormat kdbxStreamFormat = new KdbxStreamFormat(); - // load it again - try (FileInputStream f = new FileInputStream("testOutput/CHACHA-AES-CHACHA.kdbx")) { - DomDatabaseWrapper.load(kdbxStreamFormat, credentials, f); - } + kdbxDatabase.save(formatV4, credentials, v4OutputStream); + } - assertEquals("CHA_CHA_20", kdbxStreamFormat.getStreamConfiguration().getCipherAlgorithm().getName()); - assertEquals("AES", kdbxStreamFormat.getStreamConfiguration().getKeyDerivationFunction().getName()); - assertEquals("CHA_CHA_20", kdbxStreamFormat.getStreamConfiguration().getProtectedStreamAlgorithm().name()); - try (FileOutputStream f = new FileOutputStream("testOutput/CHACHA-AES-CHACHA-2.kdbx")) { - kdbxDatabase.save(kdbxStreamFormat, credentials, f); + /** + * Load KDBX V3 save as KDBX V4 + */ + public void loadKdbx4SaveKdbx3(String resourceName, byte[] password, OutputStream v3OutputStream) throws IOException { + DomDatabaseWrapper database; + // password credentials + KdbxCreds credentials = new KdbxCreds(password); + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resourceName)) { + // load KdbDatabase + database = DomDatabaseWrapper.load(credentials, inputStream); } - // doesn't matter what we create it will be overwritten - KdbxStreamFormat kdbxStreamFormat2 = new KdbxStreamFormat(); - // load it again - try (FileInputStream f = new FileInputStream("testOutput/CHACHA-AES-CHACHA-2.kdbx")) { - DomDatabaseWrapper.load(kdbxStreamFormat2, credentials, f); - } - // still CHA-CHA_20 etc. - assertEquals("CHA_CHA_20", kdbxStreamFormat2.getStreamConfiguration().getCipherAlgorithm().getName()); - assertEquals("AES", kdbxStreamFormat.getStreamConfiguration().getKeyDerivationFunction().getName()); - assertEquals("CHA_CHA_20", kdbxStreamFormat.getStreamConfiguration().getProtectedStreamAlgorithm().name()); - } - + // create a KDBX (database + D kdbxDatabase = getDatabase(); + kdbxDatabase.setName("New Database"); + kdbxDatabase.setDescription("Migration of KDBX 4 Database to KDBX 3 Database"); + // deep copy from group (not including source group, KDB database has simulated root) + kdbxDatabase.getRootGroup().copy(database.getRootGroup()); - public KdbxHeader loadKdbxHeader(String filename) throws IOException { - KdbxHeader kdbxHeader = new KdbxHeader(); - try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(filename)) { - try { - // load KdbDatabase with no credentials, in V4 this will fail as HMac256 won't be correct - // but the header will still be populated - KdbxSerializer.createUnencryptedInputStream(new Credentials.None(), kdbxHeader, inputStream); - } catch (IllegalStateException ignored) { - } - return kdbxHeader; - } - } + // choose a stream format - V4 Kdbx and choose some algorithms + KdbxHeader kdbxHeader = new KdbxHeader(KdbxHeader.KdbxHeaderOpts.V3_AES_SALSA_20); + KdbxStreamFormat formatV3 = new KdbxStreamFormat(kdbxHeader); - public void listKdbxHeaderProperties(KdbxHeader kdbxHeader, PrintWriter printWriter) { - printWriter.format("Version: %d\n", kdbxHeader.getVersion()); - printWriter.format("Cipher Algorithm: %s\n", kdbxHeader.getCipherAlgorithm().getName()); - // AES is the only KDF in V3 - KeyDerivationFunction kdf = kdbxHeader.getVersion() == 3 ? Aes.getInstance() : kdbxHeader.getKeyDerivationFunction(); - printWriter.format("Key Derivation Function: %s\n", kdf.getName()); - printWriter.format("Inner Stream Algorithm: %s\n", kdbxHeader.getProtectedStreamAlgorithm().name()); - printWriter.flush(); - } - /** - * List Database Encryption Characteristics - */ - public void listKdbxHeaderParams () throws IOException { - PrintWriter writer = new PrintWriter(System.out); - listKdbxHeaderProperties(loadKdbxHeader("V4-AES-AES.kdbx"), writer); - listKdbxHeaderProperties(loadKdbxHeader("V4-AES-Argon2.kdbx"), writer); - listKdbxHeaderProperties(loadKdbxHeader("V4-ChaCha20-AES.kdbx"), writer); - listKdbxHeaderProperties(loadKdbxHeader("V4-ChaCha20-Argon2-Attachment.kdbx"), writer); + // save it with format options + kdbxDatabase.save(formatV3, credentials, v3OutputStream); } - } diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java index 554916a0..96ab4efb 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java @@ -31,15 +31,15 @@ */ public class SaxParse { /** - * SAX Parsing + * SAX Parsing - shows also how to decrypt values that are stored encrypted in XML (Inner Stream Encryption) */ - public void exampleSaxparsing() throws IOException, SAXException, ParserConfigurationException { - InputStream encryptedInputStream = getClass().getClassLoader().getResourceAsStream("test123.kdbx"); + public void exampleSaxparsing(String resourceName) throws IOException, SAXException, ParserConfigurationException { + InputStream encryptedInputStream = getClass().getClassLoader().getResourceAsStream(resourceName); Credentials credentials = new KdbxCreds("123".getBytes()); KdbxHeader kdbxHeader = new KdbxHeader(); try (InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, kdbxHeader, encryptedInputStream)) { // use this to decrypt the encrypted fields - final StreamEncryptor memoryProtection = new StreamEncryptor.Salsa20(kdbxHeader.getInnerRandomStreamKey()); + final StreamEncryptor valueEncryptor = kdbxHeader.getInnerStreamEncryptor(); SAXParserFactory spfactory = SAXParserFactory.newInstance(); SAXParser saxParser = spfactory.newSAXParser(); XMLReader xmlReader = saxParser.getXMLReader(); @@ -87,7 +87,7 @@ public void endElement(String uri, String localName, String qName) throws SAXExc public void characters(char[] ch, int start, int length) throws SAXException { String content = new String(ch, start, length); if (protectedContent) { - content = new String(memoryProtection.decrypt(Helpers.decodeBase64Content(content.getBytes(), false))); + content = new String(valueEncryptor.decrypt(Helpers.decodeBase64Content(content.getBytes(), false))); } System.out.print(content); protectedContent = false; diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/Util.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/Util.java new file mode 100644 index 00000000..26528b4b --- /dev/null +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/Util.java @@ -0,0 +1,61 @@ +package org.linguafranca.pwdb.kdbx; + +import com.google.common.io.CharStreams; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +public class Util { + + public static InputStream getDecryptedInputStream (String resourceName, byte [] password) throws IOException { + return getDecryptedInputStream(resourceName, password, new KdbxHeader()); + } + + public static InputStream getDecryptedInputStream (String resourceName, byte [] password, KdbxHeader header) throws IOException { + InputStream is = Util.class.getClassLoader().getResourceAsStream(resourceName); + KdbxCreds creds = new KdbxCreds(password); + return KdbxSerializer.createUnencryptedInputStream(creds, header, is); + } + + public static String streamToString(InputStream inputStream) throws IOException { + return CharStreams.toString(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + } + + /** + * Example shows how to list the XML of a database + * @param resourceName the name of a resource to find on the classpath + * @param password the password for the resource + * @param printWriter a PrintWriter to list the contents + */ + public static void listXml(String resourceName, byte [] password, PrintWriter printWriter) throws IOException { + printWriter.format(resourceName + "\n"); + printWriter.println(streamToString(getDecryptedInputStream(resourceName, password))); + printWriter.println(); + printWriter.flush(); + } + + /** + * Example shows how to list KdbxParameters of a KdbxHeader. You don't need a password as the + * header is not encrypted. From V4 InnerStream encryption is defined in the Inner Header + * which is stored encrypted and is not read until the database is read. + * @param resourceName the name of a resource to find on the classpath + * @param printWriter a PrintWriter to list the contents + */ + public static void listKdbxHeaderProperties(String resourceName, PrintWriter printWriter) throws IOException { + printWriter.format(resourceName + "\n"); + KdbxHeader kdbxHeader = new KdbxHeader(); + InputStream is = Util.class.getClassLoader().getResourceAsStream(resourceName); + KdbxSerializer.readOuterHeader(is, kdbxHeader); + printWriter.format("Version: %d\n", kdbxHeader.getVersion()); + printWriter.format("Cipher Algorithm: %s\n", kdbxHeader.getCipherAlgorithm().getName()); + printWriter.format("Key Derivation Function: %s\n", kdbxHeader.getKeyDerivationFunction().getName()); + String pseName = kdbxHeader.getVersion() == 3 ? kdbxHeader.getProtectedStreamAlgorithm().name() : + "Inner Stream Algorithm not in header in V4"; + printWriter.format("Inner Stream Algorithm: %s\n", pseName); + printWriter.println(); + printWriter.flush(); + } +} diff --git a/example/src/test/java/org/linguafranca/pwdb/example/SimpleQuickStartTest.java b/example/src/test/java/org/linguafranca/pwdb/example/SimpleQuickStartTest.java deleted file mode 100644 index d92511c0..00000000 --- a/example/src/test/java/org/linguafranca/pwdb/example/SimpleQuickStartTest.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.pwdb.example; - -import org.junit.Ignore; -import org.junit.Test; -import org.linguafranca.pwdb.kdbx.QuickStart; -import org.linguafranca.pwdb.kdbx.dom.DomDatabaseWrapper; -import org.linguafranca.pwdb.kdbx.dom.DomEntryWrapper; -import org.linguafranca.pwdb.kdbx.dom.DomGroupWrapper; -import org.linguafranca.pwdb.kdbx.dom.DomIconWrapper; -import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; -import org.linguafranca.pwdb.kdbx.simple.SimpleEntry; -import org.linguafranca.pwdb.kdbx.simple.SimpleGroup; -import org.linguafranca.pwdb.kdbx.simple.SimpleIcon; -import org.linguafranca.pwdb.Credentials; - -import java.io.IOException; -import java.io.InputStream; - -/** - * Simple illustration of hooking a SAX parser up to process a KDBX file - * - * @author jo - */ -public class SimpleQuickStartTest extends QuickStart { - - - @Override - public DomDatabaseWrapper getDatabase() { - return new DomDatabaseWrapper(); - } - - @Override - public DomDatabaseWrapper loadDatabase(Credentials credentials, InputStream inputStream){ - try { - return DomDatabaseWrapper.load(credentials, inputStream); - } catch (Exception e) { - throw new IllegalStateException(e); - } - } - - @Test - public void saveTest() throws IOException { - super.saveKdbx(); - } - - @Test - public void loadTest() throws IOException { - super.loadKdbx(); - } - - @Test - public void loadKdbSaveVernacular() throws IOException { - super.loadKdb(); - } - - @Test - public void loadSaveLoadSave() throws IOException { - loadKdbx3SaveKdbx4(); - } - @Test - public void listKdbxParamsTest() throws IOException { - listKdbxHeaderParams(); - } - -} diff --git a/example/src/test/java/org/linguafranca/pwdb/example/SaxParseTest.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/SaxParseTest.java similarity index 68% rename from example/src/test/java/org/linguafranca/pwdb/example/SaxParseTest.java rename to example/src/test/java/org/linguafranca/pwdb/kdbx/SaxParseTest.java index 74d660a7..f3e7f005 100644 --- a/example/src/test/java/org/linguafranca/pwdb/example/SaxParseTest.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/SaxParseTest.java @@ -14,10 +14,9 @@ * limitations under the License. */ -package org.linguafranca.pwdb.example; +package org.linguafranca.pwdb.kdbx; import org.junit.Test; -import org.linguafranca.pwdb.kdbx.SaxParse; import org.xml.sax.SAXException; import javax.xml.parsers.ParserConfigurationException; @@ -29,8 +28,12 @@ public class SaxParseTest extends SaxParse { @Test - public void exampleSaxparsing () throws IOException, SAXException, ParserConfigurationException { - super.exampleSaxparsing(); + public void exampleSaxparsingV3 () throws IOException, SAXException, ParserConfigurationException { + super.exampleSaxparsing("test123.kdbx"); + } + @Test + public void exampleSaxparsingV4 () throws IOException, SAXException, ParserConfigurationException { + super.exampleSaxparsing("V4-AES-Argon2-CustomIcon.kdbx"); } } diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/SimpleQuickStartTest.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/SimpleQuickStartTest.java new file mode 100644 index 00000000..c8a36c0f --- /dev/null +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/SimpleQuickStartTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 2015 Jo Rabin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx; + +import org.junit.Test; +import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.kdbx.dom.DomDatabaseWrapper; +import org.linguafranca.pwdb.kdbx.dom.DomEntryWrapper; +import org.linguafranca.pwdb.kdbx.dom.DomGroupWrapper; +import org.linguafranca.pwdb.kdbx.dom.DomIconWrapper; +import org.linguafranca.pwdb.security.Encryption; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.Assert.assertEquals; + +/** + * Simple illustration of hooking a SAX parser up to process a KDBX file + * + * @author jo + */ +public class SimpleQuickStartTest extends QuickStart { + + + @Override + public DomDatabaseWrapper getDatabase() { + return new DomDatabaseWrapper(); + } + + @Override + public DomDatabaseWrapper loadDatabase(Credentials credentials, InputStream inputStream){ + try { + return DomDatabaseWrapper.load(credentials, inputStream); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + @Test + public void saveTest() throws IOException { + super.saveKdbx(); + } + + @Test + public void loadTest() throws IOException { + super.loadKdbx(); + } + + @Test + public void loadKdbSaveVernacular() throws IOException { + super.loadKdb(); + } + + @Test + public void loadSave() throws IOException { + Path path = Paths.get("testOutput/CHACHA-AES-CHACHA.kdbx"); + + loadKdbx3SaveKdbx4("test123.kdbx","123".getBytes(), Files.newOutputStream(path)); + + // load newly created V4 database + KdbxStreamFormat streamFormat = new KdbxStreamFormat(); + DomDatabaseWrapper.load(streamFormat, new KdbxCreds("123".getBytes()), Files.newInputStream(path)); + assertEquals(4, streamFormat.getStreamConfiguration().getVersion()); + assertEquals(Encryption.Cipher.CHA_CHA_20, streamFormat.getStreamConfiguration().getCipherAlgorithm()); + assertEquals(Encryption.KeyDerivationFunction.ARGON2, streamFormat.getStreamConfiguration().getKeyDerivationFunction()); + assertEquals(Encryption.ProtectedStreamAlgorithm.CHA_CHA_20, streamFormat.getStreamConfiguration().getProtectedStreamAlgorithm()); + } + @Test + public void loadSave2() throws IOException { + Path path = Paths.get("testOutput/CHACHA-AES-CHACHA.kdbx"); + + loadKdbx4SaveKdbx3("V4-ChaCha20-Argon2-Attachment.kdbx","123".getBytes(), Files.newOutputStream(path)); + + // load newly created V4 database + KdbxStreamFormat streamFormat = new KdbxStreamFormat(); + DomDatabaseWrapper.load(streamFormat, new KdbxCreds("123".getBytes()), Files.newInputStream(path)); + assertEquals(3, streamFormat.getStreamConfiguration().getVersion()); + assertEquals(Encryption.Cipher.AES, streamFormat.getStreamConfiguration().getCipherAlgorithm()); + assertEquals(Encryption.KeyDerivationFunction.AES, streamFormat.getStreamConfiguration().getKeyDerivationFunction()); + assertEquals(Encryption.ProtectedStreamAlgorithm.SALSA_20, streamFormat.getStreamConfiguration().getProtectedStreamAlgorithm()); + } +} diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java new file mode 100644 index 00000000..b77c7251 --- /dev/null +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java @@ -0,0 +1,33 @@ +package org.linguafranca.pwdb.kdbx; + +import org.junit.Test; + +import java.io.IOException; +import java.io.PrintWriter; + +import static org.linguafranca.pwdb.kdbx.Util.listKdbxHeaderProperties; +import static org.linguafranca.pwdb.kdbx.Util.listXml; + +public class UtilTest { + /** + * List Database Encryption Characteristics + */ + @Test + public void listKdbxHeaderParams () throws IOException { + PrintWriter writer = new PrintWriter(System.out); + listKdbxHeaderProperties("test123.kdbx", writer); + listKdbxHeaderProperties("V4-AES-AES.kdbx", writer); + listKdbxHeaderProperties("V4-AES-Argon2.kdbx", writer); + listKdbxHeaderProperties("V4-ChaCha20-AES.kdbx", writer); + listKdbxHeaderProperties("V4-ChaCha20-Argon2-Attachment.kdbx", writer); + writer.flush(); + } + + @Test + public void listHeaderPropertiesAndXml() throws IOException { + PrintWriter writer = new PrintWriter(System.out); + listKdbxHeaderProperties("V4-AES-Argon2-CustomIcon.kdbx", writer); + listXml("V4-AES-Argon2-CustomIcon.kdbx", "123".getBytes(), writer); + writer.flush(); + } +} diff --git a/example/src/test/java/org/linguafranca/pwdb/validation/Issue_27_Test.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_27_Test.java similarity index 98% rename from example/src/test/java/org/linguafranca/pwdb/validation/Issue_27_Test.java rename to example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_27_Test.java index da945292..30d594ea 100644 --- a/example/src/test/java/org/linguafranca/pwdb/validation/Issue_27_Test.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_27_Test.java @@ -1,4 +1,4 @@ -package org.linguafranca.pwdb.validation; +package org.linguafranca.pwdb.kdbx.validation; import com.google.common.io.CharStreams; import org.junit.Ignore; diff --git a/example/src/test/java/org/linguafranca/pwdb/validation/Issue_38_Test.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_38_Test.java similarity index 95% rename from example/src/test/java/org/linguafranca/pwdb/validation/Issue_38_Test.java rename to example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_38_Test.java index dd256347..43066e61 100644 --- a/example/src/test/java/org/linguafranca/pwdb/validation/Issue_38_Test.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_38_Test.java @@ -1,4 +1,4 @@ -package org.linguafranca.pwdb.validation; +package org.linguafranca.pwdb.kdbx.validation; import org.junit.Test; import org.linguafranca.pwdb.kdbx.KdbxCreds; diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV3Test.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV3Test.java index d7591bb3..802703cf 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV3Test.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV3Test.java @@ -46,6 +46,10 @@ public Database loadDatabase(Credentials credentials, InputStream inputStream) t return JaxbDatabase.load(credentials, inputStream); } + @Override + public Database newDatabase() { + return new JaxbDatabase(); + } @Override public Credentials getCreds(byte[] creds) { return new KdbxCreds(creds); diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.java index c2f6755f..9a9fcd26 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.java @@ -51,6 +51,11 @@ public Database loadDatabase(Credentials credentials, InputStream inputStream) t return JaxbDatabase.load(credentials, inputStream); } + @Override + public Database newDatabase() { + return new JaxbDatabase(); + } + @Override public Credentials getCreds(byte[] creds) { return new KdbxCreds(creds); diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java index ca7956b5..d6026b38 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java @@ -126,8 +126,8 @@ interface KdbxHeaderOptions { * Default values for crypto options */ enum KdbxHeaderOpts implements KdbxHeaderOptions{ - V3_AES_SALSA_20(3, Encryption.Cipher.AES, Encryption.Kdf.AES, Encryption.ProtectedStreamAlgorithm.SALSA_20), - V4_AES_ARGON_CHA_CHA (4, Encryption.Cipher.AES, Encryption.Kdf.ARGON2, Encryption.ProtectedStreamAlgorithm.CHA_CHA_20); + V3_AES_SALSA_20(3, Encryption.Cipher.AES, Encryption.KeyDerivationFunction.AES, Encryption.ProtectedStreamAlgorithm.SALSA_20), + V4_AES_ARGON_CHA_CHA (4, Encryption.Cipher.AES, Encryption.KeyDerivationFunction.ARGON2, Encryption.ProtectedStreamAlgorithm.CHA_CHA_20); // final int version; @@ -136,7 +136,7 @@ enum KdbxHeaderOpts implements KdbxHeaderOptions{ final Encryption.ProtectedStreamAlgorithm protectedStreamAlgorithm; - KdbxHeaderOpts(int version, Encryption.Cipher cipher, Encryption.Kdf kdf, Encryption.ProtectedStreamAlgorithm protectedStreamAlgorithm) { + KdbxHeaderOpts(int version, Encryption.Cipher cipher, Encryption.KeyDerivationFunction kdf, Encryption.ProtectedStreamAlgorithm protectedStreamAlgorithm) { this.version = version; this.algorithm = cipher; this.kdf = kdf; @@ -242,9 +242,7 @@ public InputStream createDecryptedStream(byte[] digest, InputStream inputStream) } public StreamEncryptor getInnerStreamEncryptor() { - return getVersion() == 4 ? - new StreamEncryptor.ChaCha20(getInnerRandomStreamKey()) : - new StreamEncryptor.Salsa20(getInnerRandomStreamKey()); + return Encryption.ProtectedStreamAlgorithm.getStreamEncryptor(getProtectedStreamAlgorithm(), getInnerRandomStreamKey()); } /** @@ -257,7 +255,7 @@ public byte[] getTransformedKeyDigest(byte[] digest) { if (kdfParameters == null) { return Aes.getTransformedKey(digest, transformSeed, transformRounds); } - KeyDerivationFunction kdf = Encryption.Kdf.getKdf(kdfParameters.mustGet("$UUID").asUuid()); + KeyDerivationFunction kdf = Encryption.KeyDerivationFunction.getKdf(kdfParameters.mustGet("$UUID").asUuid()); return kdf.getTransformedKey(digest, kdfParameters); } @@ -425,7 +423,7 @@ public void setVersion(int version) { */ public void setKdfParameters(VariantDictionary kdfParameters) { this.kdfParameters = kdfParameters; - this.keyDerivationFunction = Encryption.Kdf.getKdf(kdfParameters.get("$UUID").asUuid()); + this.keyDerivationFunction = Encryption.KeyDerivationFunction.getKdf(kdfParameters.get("$UUID").asUuid()); } /** diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java index 517752b6..22a17dd2 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java @@ -79,6 +79,7 @@ public void load(SerializableDatabase serializableDatabase, Credentials credenti @Override public void save(SerializableDatabase serializableDatabase, Credentials credentials, OutputStream encryptedOutputStream) throws IOException { if (kdbxHeader.getVersion() == 4) { + // TODO this assumes that the indexes start from 0 and are in sequence ... for (int a = 0; a < serializableDatabase.getBinaryCount(); a++) { int attachmentLength = serializableDatabase.getBinary(a).length; byte[] binary = new byte[attachmentLength + 1]; diff --git a/pom.xml b/pom.xml index 0116f611..37f64900 100644 --- a/pom.xml +++ b/pom.xml @@ -153,11 +153,6 @@ bcpkix-jdk15on 1.70 - - com.google.code.gson - gson - 2.10.1 - org.slf4j slf4j-api diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java index c9b97cc8..aca2335d 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java @@ -51,6 +51,11 @@ public Database loadDatabase(Credentials credentials, InputStream inputStream) t } } + @Override + public Database newDatabase() { + return new SimpleDatabase(); + } + @Override public Credentials getCreds(byte[] creds) { return new KdbxCreds(creds); diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java index b80acc29..26af6d14 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java @@ -59,6 +59,7 @@ public abstract class BinaryPropertyChecks { @SuppressWarnings("unused") public abstract void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException; public abstract Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException; + public abstract Database newDatabase(); public abstract Credentials getCreds(byte[] creds); /** @@ -161,5 +162,27 @@ public void saveAndReloadCheck() throws IOException { } + /** + * Checks that a new database can add binary properties saves and reloads correctly + */ + @Test + public void createAndSaveCheck() throws IOException { + Path file = Files.createTempFile("keepass", "tmp"); + Database database1 = newDatabase(); + Entry entry = database1.newEntry("Test attachment"); + database1.getRootGroup().addEntry(entry); + InputStream testfile = getClass().getClassLoader().getResourceAsStream("letter J.jpeg"); + byte [] letterJ = ByteStreams.toByteArray(testfile); + entry.setBinaryProperty("letter J.jpeg", letterJ); + saveDatabase(database1, getCreds("123".getBytes()), Files.newOutputStream(file)); + + Database db = loadDatabase(getCreds("123".getBytes()),Files.newInputStream(file)); + Entry entry1 = (Entry) db.findEntries("Test attachment").get(0); + // just one property + assertArrayEquals(new String[] {"letter J.jpeg"}, entry1.getBinaryPropertyNames().toArray()); + // content is correct + assertArrayEquals(letterJ, entry1.getBinaryProperty("letter J.jpeg")); + } + } diff --git a/test/src/main/java/org/linguafranca/util/HexViewer.java b/test/src/main/java/org/linguafranca/util/HexViewer.java new file mode 100644 index 00000000..e68a779e --- /dev/null +++ b/test/src/main/java/org/linguafranca/util/HexViewer.java @@ -0,0 +1,32 @@ +package org.linguafranca.util; + +import java.io.IOException; +import java.io.InputStream; + +public class HexViewer { + + public static void main(String[] args) throws IOException { + + + try (InputStream is = HexViewer.class.getClassLoader().getResourceAsStream("V4-AES-Argon2-CustomIcon.kdbx");) { + + //HexFormat format = HexFormat.ofDelimiter(" "); + + for (int i = 0; i < 32; i++) { + byte[] buf = new byte [16]; + is.read(buf); + StringBuilder sb = new StringBuilder(); + for (byte b: buf) { + sb.append(String.format("%02X", b)); + } + sb.append(" "); + for (byte b : buf) { + sb.append(b < 0x20 || b > 0x7e ? (char) 0x00B7 : (char) b); + } + System.out.println(sb); + } + } + } +} + + diff --git a/test/src/main/resources/V4-AES-Argon2-CustomIcon.kdbx b/test/src/main/resources/V4-AES-Argon2-CustomIcon.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..d4a49c7aa3f30e7a3a740753e41e7b35e12745b1 GIT binary patch literal 26757 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 literal 0 HcmV?d00001 From 1e47e247d4afc4c9e5518eb313c4d430f20c1f43 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Wed, 10 May 2023 17:52:50 +0100 Subject: [PATCH 093/228] missed from previous commit for some reason #13, #36, #39 --- .../org/linguafranca/pwdb/security/Encryption.java | 10 +++++----- .../java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java | 3 +++ .../pwdb/kdbx/simple/SimpleBinaryPropertyV3Test.java | 6 ++++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java b/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java index 3913f92c..36d4a46e 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java @@ -95,13 +95,13 @@ public static byte[] transformHmacKey(byte[] digest, byte[] transform) { * A list of functions that we can use to transform keys * Enum constants forward to underlying implementation. */ - public enum Kdf implements KeyDerivationFunction { + public enum KeyDerivationFunction implements org.linguafranca.pwdb.security.KeyDerivationFunction { AES(Aes.getInstance()), ARGON2(Argon2.getInstance()); - private final KeyDerivationFunction kdf; + private final org.linguafranca.pwdb.security.KeyDerivationFunction kdf; - Kdf(KeyDerivationFunction kdf) { + KeyDerivationFunction(org.linguafranca.pwdb.security.KeyDerivationFunction kdf) { this.kdf = kdf; } @@ -111,8 +111,8 @@ public enum Kdf implements KeyDerivationFunction { * @param kdfUuid the Uuid to match * @throws IllegalArgumentException if the Uuid is not known */ - public static KeyDerivationFunction getKdf(UUID kdfUuid) { - for (KeyDerivationFunction kdf : values()) { + public static org.linguafranca.pwdb.security.KeyDerivationFunction getKdf(UUID kdfUuid) { + for (org.linguafranca.pwdb.security.KeyDerivationFunction kdf : values()) { if (kdf.getKdfUuid().equals(kdfUuid)) { return kdf; } diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java index 268f4cf1..d1cd48af 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java @@ -198,6 +198,9 @@ static Element setBinaryElementContent(String elementPath, Element parentElement //Find the highest numbered existing content String max = xpath.evaluate("//Binaries/Binary/@ID[not(. < ../../Binary/@ID)][1]", parentElement.getOwnerDocument().getDocumentElement()); + if (max.trim().isEmpty()){ + max = "-1"; + } Integer newIndex = Integer.valueOf(max) + 1; addBinary(parentElement.getOwnerDocument().getDocumentElement(), b64, newIndex); diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV3Test.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV3Test.java index 69d78695..be8bde9b 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV3Test.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV3Test.java @@ -54,6 +54,12 @@ public Database loadDatabase(Credentials credentials, InputStream inputStream) t } } + + @Override + public Database newDatabase() { + return new SimpleDatabase(); + } + @Override public Credentials getCreds(byte[] creds) { return new KdbxCreds(creds); From f9849be30c990dc41f1e0bbcc026ab8aba4b28ca Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Wed, 10 May 2023 22:26:53 +0100 Subject: [PATCH 094/228] Rework of SimpleDatabase to ensure save respects encryption of file when loaded #36 --- .../pwdb/SerializableDatabase.java | 3 +- .../pwdb/kdbx/KdbxStreamFormat.java | 8 +- .../pwdb/kdbx/simple/SimpleDatabase.java | 293 ++++++------------ .../simple/SimpleSerializableDatabase.java | 207 +++++++++++++ .../pwdb/checks/BasicDatabaseChecks.java | 72 ++--- .../pwdb/checks/BinaryPropertyChecks.java | 77 ++--- 6 files changed, 378 insertions(+), 282 deletions(-) create mode 100644 simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleSerializableDatabase.java diff --git a/database/src/main/java/org/linguafranca/pwdb/SerializableDatabase.java b/database/src/main/java/org/linguafranca/pwdb/SerializableDatabase.java index 1fd8d9e5..b61bedd9 100644 --- a/database/src/main/java/org/linguafranca/pwdb/SerializableDatabase.java +++ b/database/src/main/java/org/linguafranca/pwdb/SerializableDatabase.java @@ -23,7 +23,8 @@ import java.io.OutputStream; /** - * This interface allows for serialization and deserialization of databases. + * This interface allows for serialization and deserialization of databases, which is + * theoretically decoupled from the actual format they use for serialization. *

        * Databases instantiate themselves from a stream and serialize to a stream, * and need to be able to encrypt and decrypt data (e.g. Protected fields in KDBX format). diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java index 22a17dd2..2164de49 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java @@ -77,7 +77,7 @@ public void load(SerializableDatabase serializableDatabase, Credentials credenti } @Override - public void save(SerializableDatabase serializableDatabase, Credentials credentials, OutputStream encryptedOutputStream) throws IOException { + public void save(SerializableDatabase serializableDatabase, Credentials credentials, OutputStream outputStream) throws IOException { if (kdbxHeader.getVersion() == 4) { // TODO this assumes that the indexes start from 0 and are in sequence ... for (int a = 0; a < serializableDatabase.getBinaryCount(); a++) { @@ -89,13 +89,13 @@ public void save(SerializableDatabase serializableDatabase, Credentials credenti } } - try (OutputStream unencrytedOutputStream = KdbxSerializer.createEncryptedOutputStream(credentials, kdbxHeader, encryptedOutputStream)) { + try (OutputStream encryptedOutputStream = KdbxSerializer.createEncryptedOutputStream(credentials, kdbxHeader, outputStream)) { if (kdbxHeader.getVersion() == 3) { serializableDatabase.setHeaderHash(kdbxHeader.getHeaderHash()); } serializableDatabase.setEncryption(kdbxHeader.getStreamEncryptor()); - serializableDatabase.save(unencrytedOutputStream); - unencrytedOutputStream.flush(); + serializableDatabase.save(encryptedOutputStream); + encryptedOutputStream.flush(); } } diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java index 1cc047d3..90c1f956 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java @@ -16,32 +16,24 @@ package org.linguafranca.pwdb.kdbx.simple; +import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.StreamConfiguration; import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.base.AbstractDatabase; -import org.linguafranca.pwdb.kdbx.Helpers; -import org.linguafranca.pwdb.security.StreamEncryptor; -import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxInputTransformer; -import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxOutputTransformer; import org.linguafranca.pwdb.kdbx.KdbxHeader; -import org.linguafranca.pwdb.kdbx.KdbxSerializer; -import org.linguafranca.pwdb.kdbx.simple.converter.*; -import org.linguafranca.pwdb.kdbx.simple.model.EntryClasses; +import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import org.linguafranca.pwdb.kdbx.simple.model.KeePassFile; -import org.linguafranca.pwdb.Credentials; -import org.linguafranca.xml.XmlInputStreamFilter; -import org.linguafranca.xml.XmlOutputStreamFilter; -import org.simpleframework.xml.*; -import org.simpleframework.xml.convert.AnnotationStrategy; -import org.simpleframework.xml.convert.Registry; -import org.simpleframework.xml.convert.RegistryStrategy; -import org.simpleframework.xml.core.Persister; -import org.simpleframework.xml.strategy.Strategy; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.*; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import static org.linguafranca.pwdb.kdbx.simple.SimpleSerializableDatabase.createEmptyDatabase; +import static org.linguafranca.pwdb.kdbx.simple.SimpleSerializableDatabase.getSerializer; /** * Implementation of {@link org.linguafranca.pwdb.Database} using the Simple XML framework. @@ -52,29 +44,99 @@ public class SimpleDatabase extends AbstractDatabase{ KeePassFile keePassFile; + StreamFormat streamFormat; + private SimpleSerializableDatabase serializableDatabase; /** * Create a new empty database */ public SimpleDatabase() { + this(createEmptyDatabase()); + } + + public SimpleDatabase(KeePassFile file) { try { - keePassFile = createEmptyDatabase(); + keePassFile = file; keePassFile.root.group.database = this; + this.serializableDatabase = new SimpleSerializableDatabase(); + this.serializableDatabase.setKeePassFile(this.keePassFile); + SimpleSerializableDatabase.fixUp(keePassFile.root.group); } catch (Exception e) { throw new IllegalStateException(e); } } + /** + * Load plaintext XML + * + * @param inputStream contains the XML + * @return a new Database + * @throws Exception on load failure + */ + public static SimpleDatabase loadXml(InputStream inputStream) throws Exception { + KeePassFile result = getSerializer().read(KeePassFile.class, inputStream); + result.root.group.uuid = UUID.randomUUID(); + return new SimpleDatabase(result); + } /** - * Create a database instance from a keepass file - * @param keePassFile the instance to initialise from + * Load kdbx file + * + * @param credentials the credentials to use + * @param inputStream the encrypted input stream + * @return a new database */ - protected SimpleDatabase (KeePassFile keePassFile) { - this.keePassFile = keePassFile; - this.keePassFile.root.group.database = this; - fixUp(this.keePassFile.root.group); + public static SimpleDatabase load(Credentials credentials, InputStream inputStream) throws IOException { + // doesn't matter how streamFormat is initialised + return load(new KdbxStreamFormat(), credentials, inputStream); } + /** + * Load file with a choice of StreamFormat + * @param streamFormat which contains format information about the file after load + * @param credentials credentials to use + * @param inputStream where to load from + * @return a new database + * @param config for the streamFormat + */ + public static SimpleDatabase load(StreamFormat streamFormat, + Credentials credentials, + InputStream inputStream) throws IOException { + SimpleSerializableDatabase simpleSerializableDatabase = new SimpleSerializableDatabase(); + streamFormat.load(simpleSerializableDatabase, credentials, inputStream); + SimpleDatabase db = new SimpleDatabase(simpleSerializableDatabase.getKeePassFile()); + db.serializableDatabase = simpleSerializableDatabase; + db.streamFormat = streamFormat; + return db; + } + + /** + * Save the database with the same stream format that it was loaded with, or V4 default if none + * @param credentials credentials to use + * @param outputStream where to write to + */ + @Override + public void save(Credentials credentials, OutputStream outputStream) throws IOException { + // save with the stream format used to load if it exists, otherwise save V4 + if (Objects.isNull(streamFormat)) { + streamFormat = new KdbxStreamFormat(new KdbxHeader(4)); + } + save(streamFormat, credentials, outputStream); + } + + /** + * Save the database with a choice of stream format + * @param streamFormat the format to use + * @param credentials credentials to use + * @param outputStream where to write to + */ + @Override + public void save(StreamFormat streamFormat, Credentials credentials, + OutputStream outputStream) throws IOException{ + streamFormat.save(this.serializableDatabase, credentials, outputStream); + setDirty(false); + } + + @Override public SimpleGroup getRootGroup() { return keePassFile.root.getGroup(); @@ -149,142 +211,6 @@ public void setDescription(String s) { setDirty(true); } - /** - * Create an empty underlying KeePassFile instance - * - * @return a new database - * @throws Exception on failure - */ - private static KeePassFile createEmptyDatabase() throws Exception { - InputStream inputStream = SimpleDatabase.class.getClassLoader().getResourceAsStream("base.kdbx.xml"); - return getSerializer().read(KeePassFile.class, inputStream); - } - - /** - * Load plaintext XML - * - * @param inputStream contains the XML - * @return a new Database - * @throws Exception on load failure - */ - public static SimpleDatabase loadXml(InputStream inputStream) throws Exception { - KeePassFile result = getSerializer().read(KeePassFile.class, inputStream); - result.root.group.uuid = UUID.randomUUID(); - return new SimpleDatabase(result); - } - - /** - * Load kdbx file - * - * @param credentials the credentials to use - * @param inputStream the encrypted input stream - * @return a new database - * @throws Exception on load failure - */ - public static SimpleDatabase load(Credentials credentials, InputStream inputStream) throws Exception { - - // load the KDBX header and get the inner Kdbx stream - KdbxHeader kdbxHeader = new KdbxHeader(); - InputStream kdbxInnerStream = KdbxSerializer.createUnencryptedInputStream(credentials, kdbxHeader, inputStream); - - StreamEncryptor streamEncyptor = kdbxHeader.getInnerStreamEncryptor(); - - // decrypt the encrypted fields in the inner XML stream - InputStream plainTextXmlStream = new XmlInputStreamFilter(kdbxInnerStream, new KdbxInputTransformer(streamEncyptor)); - - // read the now entirely decrypted stream into database - KeePassFile result = getSerializer().read(KeePassFile.class, plainTextXmlStream); - - if (kdbxHeader.getVersion() == 3 && !Arrays.equals(result.meta.headerHash.getContent(), kdbxHeader.getHeaderHash())) { - throw new IllegalStateException("Header Hash Mismatch"); - } - - if (kdbxHeader.getVersion() == 4) { - int index = 0; - for (byte[] binary : kdbxHeader.getBinaries()) { - addBinary(result, Arrays.copyOfRange(binary, 1, binary.length), index); - index++; - } - } - - return new SimpleDatabase(result); - } - - public void addBinary(byte[] bytes, Integer index) { - addBinary(keePassFile, bytes, index); - } - - public static void addBinary(KeePassFile keePassFile, byte[] bytes, Integer index) { - // create a new binary to put in the store - KeePassFile.Binary newBin = new KeePassFile.Binary(); - newBin.setId(index); - newBin.setValue(Helpers.encodeBase64Content(bytes, true)); - newBin.setCompressed(true); - if (keePassFile.getBinaries() == null) { - keePassFile.createBinaries(); - } - keePassFile.getBinaries().add(newBin); - } - - - /** - * Save as plaintext XML - * - * @param outputStream the destination to save to - */ - public void save(OutputStream outputStream) { - try { - prepareForSave(keePassFile.root.group); - - // and save the database out - getSerializer().write(this.keePassFile, outputStream); - - } catch (Exception e) { - throw new IllegalStateException(e); - } - - } - - @Override - public void save(Credentials credentials, OutputStream outputStream) throws IOException { - try { - // create the stream to accept unencrypted data and output to encrypted - KdbxHeader kdbxHeader = new KdbxHeader(); - OutputStream kdbxInnerStream = KdbxSerializer.createEncryptedOutputStream(credentials, kdbxHeader, outputStream); - - if (kdbxHeader.getVersion() == 3) { - // the database contains the hash of the headers - if (Objects.isNull(keePassFile.meta.headerHash)){ - keePassFile.meta.headerHash = new KeePassFile.ByteArray(kdbxHeader.getHeaderHash()); - } else { - keePassFile.meta.headerHash.setContent(kdbxHeader.getHeaderHash()); - } - } - - // encrypt the fields in the XML inner stream - XmlOutputStreamFilter plainTextOutputStream = new XmlOutputStreamFilter(kdbxInnerStream, - new KdbxOutputTransformer(new StreamEncryptor.Salsa20(kdbxHeader.getInnerRandomStreamKey()))); - - // set up the "protected" attributes of fields that need inner stream encryption - prepareForSave(keePassFile.root.group); - - // and save the database out - getSerializer().write(this.keePassFile, plainTextOutputStream); - plainTextOutputStream.close(); - plainTextOutputStream.await(); - this.setDirty(false); - - } catch (Exception e) { - throw new IllegalStateException(e); - } - } - - @Override - public void save(StreamFormat streamFormat, Credentials credentials, - OutputStream outputStream) throws IOException{ - throw new UnsupportedOperationException(); - } - @Override public boolean shouldProtect(String s) { return keePassFile.meta.memoryProtection.shouldProtect(s); @@ -295,50 +221,7 @@ public List getBinaries() { return keePassFile.getBinaries(); } - /** - * Utility to get a simple framework persister - * @return a persister - * @throws Exception when things get tough - */ - private static Serializer getSerializer() throws Exception { - Registry registry = new Registry(); - registry.bind(String.class, EmptyStringConverter.class); - Strategy strategy = new AnnotationStrategy(new RegistryStrategy(registry)); - return new Persister(strategy); - - } - - /** - * Utility to add in back links to parent group and database - * - * @param parent the group to start from - */ - private static void fixUp(SimpleGroup parent){ - for (SimpleGroup group: parent.group) { - group.parent = parent; - group.database = parent.database; - fixUp(group); - } - for (SimpleEntry entry: parent.entry) { - entry.database = parent.database; - entry.parent = parent; - } - } - - /** - * Utility to mark fields that need to be encrypted and vice versa - * - * @param parent the group to start from - */ - private static void prepareForSave(SimpleGroup parent){ - for (SimpleGroup group: parent.group) { - prepareForSave(group); - } - for (SimpleEntry entry: parent.entry) { - for (EntryClasses.StringProperty property : entry.string) { - boolean shouldProtect = parent.database.shouldProtect(property.getKey()); - property.getValue().setProtected(shouldProtect); - } - } + public void addBinary(byte [] bytes, int index) { + SimpleSerializableDatabase.addBinary(this.keePassFile, index, bytes); } } \ No newline at end of file diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleSerializableDatabase.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleSerializableDatabase.java new file mode 100644 index 00000000..a3d1ee61 --- /dev/null +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleSerializableDatabase.java @@ -0,0 +1,207 @@ +/* + * Copyright 2015 Jo Rabin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx.simple; + +import org.linguafranca.pwdb.SerializableDatabase; +import org.linguafranca.pwdb.kdbx.Helpers; +import org.linguafranca.pwdb.kdbx.simple.converter.EmptyStringConverter; +import org.linguafranca.pwdb.kdbx.simple.model.EntryClasses; +import org.linguafranca.pwdb.kdbx.simple.model.KeePassFile; +import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxInputTransformer; +import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxOutputTransformer; +import org.linguafranca.pwdb.security.StreamEncryptor; +import org.linguafranca.xml.XmlInputStreamFilter; +import org.linguafranca.xml.XmlOutputStreamFilter; +import org.simpleframework.xml.Serializer; +import org.simpleframework.xml.convert.AnnotationStrategy; +import org.simpleframework.xml.convert.Registry; +import org.simpleframework.xml.convert.RegistryStrategy; +import org.simpleframework.xml.core.Persister; +import org.simpleframework.xml.strategy.Strategy; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Objects; + +/** + * @author jo + */ +@SuppressWarnings("WeakerAccess") +public class SimpleSerializableDatabase implements SerializableDatabase { + + public KeePassFile keePassFile; + private StreamEncryptor encryption; + + + /** + * Create an empty underlying KeePassFile instance + * + * @return a new database + */ + static KeePassFile createEmptyDatabase() { + InputStream inputStream = SimpleDatabase.class.getClassLoader().getResourceAsStream("base.kdbx.xml"); + try { + return getSerializer().read(KeePassFile.class, inputStream); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public SimpleSerializableDatabase load(InputStream inputStream) { + try { + // decrypt the encrypted fields in the inner XML stream + InputStream plainTextXmlStream = new XmlInputStreamFilter(inputStream, new KdbxInputTransformer(encryption)); + // read the now entirely decrypted stream into database + keePassFile = getSerializer().read(KeePassFile.class, plainTextXmlStream); + // ensure that parent fields are set + fixUp(keePassFile.root.group); + return this; + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + @Override + public void save(OutputStream outputStream) throws IOException { + // encrypt the fields in the XML inner stream + XmlOutputStreamFilter plainTextOutputStream = new XmlOutputStreamFilter(outputStream, new KdbxOutputTransformer(encryption)); + + // set up the "protected" attributes of fields that need inner stream encryption + prepareForSave(keePassFile.root.group); + + // and save the database out + try { + getSerializer().write(this.keePassFile, plainTextOutputStream); + } catch (Exception e) { + throw new IOException(e); + } + plainTextOutputStream.close(); + plainTextOutputStream.await(); + } + + /** + * Utility to get a simple framework persister + * @return a persister + */ + static Serializer getSerializer() { + Registry registry = new Registry(); + try { + registry.bind(String.class, EmptyStringConverter.class); + } catch (Exception e) { + throw new IllegalStateException(e); + } + Strategy strategy = new AnnotationStrategy(new RegistryStrategy(registry)); + return new Persister(strategy); + } + + /** + * Utility to mark fields that need to be encrypted and vice versa + * + * @param parent the group to start from + */ + private static void prepareForSave(SimpleGroup parent){ + for (SimpleGroup group: parent.group) { + prepareForSave(group); + } + for (SimpleEntry entry: parent.entry) { + for (EntryClasses.StringProperty property : entry.string) { + boolean shouldProtect = parent.database.shouldProtect(property.getKey()); + property.getValue().setProtected(shouldProtect); + } + } + } + + /** + * On load add parents + * @param parent a parent to recurse + */ + static void fixUp(SimpleGroup parent){ + for (SimpleGroup group: parent.group) { + group.parent = parent; + group.database = parent.database; + fixUp(group); + } + for (SimpleEntry entry: parent.entry) { + entry.database = parent.database; + entry.parent = parent; + } + } + + @Override + public StreamEncryptor getEncryption() { + return encryption; + } + + @Override + public void setEncryption(StreamEncryptor encryption) { + this.encryption = encryption; + } + + @Override + public byte[] getHeaderHash() { + return keePassFile.meta.headerHash.getContent(); + } + + @Override + public void setHeaderHash(byte[] hash) { + keePassFile.meta.headerHash = new KeePassFile.ByteArray(hash); + } + + @Override + public void addBinary(int index, byte[] value) { + addBinary(keePassFile, index, value); + } + + @Override + public byte[] getBinary(int index) { + KeePassFile.Binary binary = keePassFile.getBinaries().get(index); + String value = binary.getValue(); + return Helpers.decodeBase64Content(value.getBytes(), binary.getCompressed()); + } + + @Override + public int getBinaryCount() { + if (Objects.isNull(keePassFile.getBinaries())){ + return 0; + } + return keePassFile.getBinaries().size(); + } + + public static void addBinary(KeePassFile keePassFile, int index, byte[] value) { + // create a new binary to put in the store + KeePassFile.Binary newBin = new KeePassFile.Binary(); + newBin.setId(index); + newBin.setValue(Helpers.encodeBase64Content(value, true)); + newBin.setCompressed(true); + if (keePassFile.getBinaries() == null) { + keePassFile.createBinaries(); + } + keePassFile.getBinaries().add(newBin); + } + + + + public KeePassFile getKeePassFile() { + return keePassFile; + } + + public void setKeePassFile(KeePassFile keypassFile) { + this.keePassFile = keypassFile; + } +} diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java index 38eb4e2a..cc2c3a38 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java @@ -45,21 +45,21 @@ public BasicDatabaseChecks() throws IOException { @Test public void testEmptyDatabase() { - Assert.assertTrue (database.getRootGroup().getName().equals("Root")); - Assert.assertTrue (database.getRootGroup().getEntries().size() == 0); - Assert.assertTrue (database.getRootGroup().getGroups().size() == 0); + assertEquals("Root", database.getRootGroup().getName()); + assertEquals(0, database.getRootGroup().getEntries().size()); + assertEquals(0, database.getRootGroup().getGroups().size()); } @Test public void testAddGroup() { - Group g1 = database.getRootGroup().addGroup(database.newGroup("group1")); - Assert.assertTrue (database.getRootGroup().getGroups().size() == 1); - Assert.assertTrue (g1.getName().equals("group1")); - Assert.assertTrue (g1.getGroups().size() == 0); - Assert.assertTrue (g1.getEntries().size() == 0); - Assert.assertTrue("root is not the parent of its child", g1.getParent().equals(database.getRootGroup())); - - Group g2 = database.newGroup(); + G g1 = database.getRootGroup().addGroup(database.newGroup("group1")); + assertEquals(1, database.getRootGroup().getGroups().size()); + assertEquals("group1", g1.getName()); + assertEquals(0, g1.getGroups().size()); + assertEquals(0, g1.getEntries().size()); + assertEquals("root is not the parent of its child", g1.getParent(), database.getRootGroup()); + + G g2 = database.newGroup(); assertEquals("", g2.getName()); Assert.assertNotNull(g2.getUuid()); assertEquals(0, g2.getIcon().getIndex()); @@ -68,22 +68,22 @@ public void testAddGroup() { // show that the list of groups is a copy database.getRootGroup().getGroups().clear(); - Assert.assertTrue(database.getRootGroup().getGroups().size() == 1); - Assert.assertTrue (g1.getEntries().size() == 0); + assertEquals(1, database.getRootGroup().getGroups().size()); + assertEquals(0, g1.getEntries().size()); } @Test public void testDeleteGroup () { Group g1 = database.getRootGroup().addGroup(database.newGroup("group1")); List l1 = database.getRootGroup().findGroups("group1"); - Assert.assertTrue(l1.size() == 1); + assertEquals(1, l1.size()); G g2 = l1.get(0); - Assert.assertTrue (g2.equals(g1)); - Group g3 = database.getRootGroup().removeGroup(g2); - Assert.assertTrue (g3.equals(g1)); - Assert.assertTrue(g1.getParent() == null); - Assert.assertTrue(database.getRootGroup().getGroups().size() == 0); - Assert.assertTrue(database.getRootGroup().findGroups("group1").size() == 0); + assertEquals(g2, g1); + G g3 = database.getRootGroup().removeGroup(g2); + assertEquals(g3, g1); + assertNull(g1.getParent()); + assertEquals(0, database.getRootGroup().getGroups().size()); + assertEquals(0, database.getRootGroup().findGroups("group1").size()); } @Test @@ -91,38 +91,38 @@ public void testAddRemoveEntry() { E e1 = database.getRootGroup().addEntry(database.newEntry()); e1.setTitle("entry1"); List l1 = database.findEntries("entry1"); - Assert.assertTrue(l1.size() == 1); + assertEquals(1, l1.size()); E e12 = database.getRootGroup().addEntry(database.newEntry("entry12")); List l2 = database.findEntries("entry1"); - Assert.assertTrue(l2.size() == 2); + assertEquals(2, l2.size()); // show that the entries are different - Assert.assertFalse(l2.get(0).equals(l2.get(1))); + assertNotEquals(l2.get(0), l2.get(1)); // show that the list is a copy l2.clear(); - Assert.assertTrue(database.findEntries("entry1").size() == 2); + assertEquals(2, database.findEntries("entry1").size()); // show that we get an equivalent entry when we remove to when we inserted - Entry e12b = database.getRootGroup().removeEntry(e12); - Assert.assertTrue(e12b.equals(e12)); + E e12b = database.getRootGroup().removeEntry(e12); + assertEquals(e12b, e12); // has been unhooked from parent - Assert.assertTrue(e12.getParent() == null); - Assert.assertTrue(database.findEntries("entry1").size() == 1); + assertNull(e12.getParent()); + assertEquals(1, database.findEntries("entry1").size()); } @Test public void testSetFields () { E e1 = database.newEntry("Entry 1"); e1.setNotes("this looks a little like Entry 2"); - Assert.assertTrue(e1.getNotes().equals("this looks a little like Entry 2")); + assertEquals("this looks a little like Entry 2", e1.getNotes()); e1.setUsername("jake@window.com"); - Assert.assertTrue(e1.getUsername().equals("jake@window.com")); + assertEquals("jake@window.com", e1.getUsername()); e1.setPassword("supercalifragelisticexpialidocious"); - Assert.assertTrue(e1.getPassword().equals("supercalifragelisticexpialidocious")); - e1.setUrl("http://window.com"); - Assert.assertTrue(e1.getUrl().equals("http://window.com")); + assertEquals("supercalifragelisticexpialidocious", e1.getPassword()); + e1.setUrl("https://window.com"); + assertEquals("https://window.com", e1.getUrl()); Assert.assertTrue(e1.match("2")); @@ -131,7 +131,7 @@ public void testSetFields () { I ic1 = database.newIcon(27); e1.setIcon(ic1); - Assert.assertTrue(e1.getIcon().equals(ic1)); + assertEquals(e1.getIcon(), ic1); // databases have to support setting of standard properties e1.setProperty(Entry.STANDARD_PROPERTY_NAME_TITLE, "A title"); @@ -155,7 +155,7 @@ public void testSetFields () { Assert.assertEquals(0, properties.size()); } catch (UnsupportedOperationException e) { // databases don't have to support arbitrary properties - assertTrue(!database.supportsNonStandardPropertyNames()); + assertFalse(database.supportsNonStandardPropertyNames()); assertArrayEquals(e1.getPropertyNames().toArray(), Entry.STANDARD_PROPERTY_NAMES.toArray()); } @@ -237,7 +237,7 @@ public void testCopy() throws IOException { entry1.setTitle("Entry"); entry1.setUsername("Username"); entry1.setPassword("Password"); - entry1.setUrl("http://dont.follow.me"); + entry1.setUrl("https://dont.follow.me"); entry1.setNotes("Notes"); entry1.setIcon(database.newIcon(2)); diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java index 26af6d14..92998e9b 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java @@ -18,18 +18,14 @@ import com.google.common.io.ByteStreams; import org.junit.Assert; - -import org.junit.Ignore; import org.junit.Test; -import org.linguafranca.pwdb.Credentials; -import org.linguafranca.pwdb.Database; -import org.linguafranca.pwdb.Entry; - +import org.linguafranca.pwdb.*; -import java.io.*; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.attribute.FileAttribute; import static org.junit.Assert.*; @@ -44,7 +40,7 @@ *

        Contains an attachment "Letter L" which is also present in the resources directory
        * * - * When used as a test suite for a concrete implementation, subclass and name the class *Test etc to conform + * When used as a test suite for a concrete implementation, subclass and name the class *Test etc. to conform * with Junit rules *

        * Subclasses should test both V3 KDBX files (Attachment.kdbx) and V4 (V4-ChaCha20-Argon2-Attachment.kdbx) since @@ -52,14 +48,14 @@ * * @author jo */ -public abstract class BinaryPropertyChecks { +public abstract class BinaryPropertyChecks , G extends Group, E extends Entry, I extends Icon> { - public Database database; + public D database; @SuppressWarnings("unused") - public abstract void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException; - public abstract Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException; - public abstract Database newDatabase(); + public abstract void saveDatabase(D database, Credentials credentials, OutputStream outputStream) throws IOException; + public abstract D loadDatabase(Credentials credentials, InputStream inputStream) throws IOException; + public abstract D newDatabase(); public abstract Credentials getCreds(byte[] creds); /** @@ -67,10 +63,11 @@ public abstract class BinaryPropertyChecks { */ @Test public void getBinaryProperty() throws Exception { - Entry entry = database.findEntries("Test attachment").get(0); + E entry = database.findEntries("Test attachment").get(0); byte [] letterJ = entry.getBinaryProperty("letter J.jpeg"); - InputStream testfile = getClass().getClassLoader().getResourceAsStream("letter J.jpeg"); - byte [] original = ByteStreams.toByteArray(testfile); + InputStream testFile = getClass().getClassLoader().getResourceAsStream("letter J.jpeg"); + assert testFile != null; + byte [] original = ByteStreams.toByteArray(testFile); Assert.assertArrayEquals(original, letterJ); } @@ -79,10 +76,11 @@ public void getBinaryProperty() throws Exception { */ @Test public void getAnotherBinaryProperty() throws Exception { - Entry entry = database.findEntries("Test 2 attachment").get(0); + E entry = database.findEntries("Test 2 attachment").get(0); byte [] letterL = entry.getBinaryProperty("letter L.jpeg"); - InputStream testfile = getClass().getClassLoader().getResourceAsStream("letter L.jpeg"); - byte [] original = ByteStreams.toByteArray(testfile); + InputStream testFile = getClass().getClassLoader().getResourceAsStream("letter L.jpeg"); + assert testFile != null; + byte [] original = ByteStreams.toByteArray(testFile); Assert.assertArrayEquals(original, letterL); } @@ -91,9 +89,10 @@ public void getAnotherBinaryProperty() throws Exception { */ @Test public void setBinaryProperty() throws Exception { - InputStream testfile = getClass().getClassLoader().getResourceAsStream("letter L.jpeg"); - byte [] original = ByteStreams.toByteArray(testfile); - Entry entry = database.findEntries("Test attachment").get(0); + InputStream testFile = getClass().getClassLoader().getResourceAsStream("letter L.jpeg"); + assert testFile != null; + byte [] original = ByteStreams.toByteArray(testFile); + E entry = database.findEntries("Test attachment").get(0); entry.setBinaryProperty("letter L.jpeg", original); byte [] letterL = entry.getBinaryProperty("letter L.jpeg"); Assert.assertArrayEquals(original, letterL); @@ -106,7 +105,7 @@ public void setBinaryProperty() throws Exception { */ @Test public void getBinaryPropertyNames() { - Entry entry = database.findEntries("Test attachment").get(0); + E entry = database.findEntries("Test attachment").get(0); assertArrayEquals(new String[] {"letter J.jpeg"}, entry.getBinaryPropertyNames().toArray()); entry = database.findEntries("Test 2 attachment").get(0); @@ -129,7 +128,7 @@ public void checkSupported(){ public void checkAddChangeRemoveBinaryProperty() { byte[] test = new byte[] {0, 1, 2 ,3}; byte[] test2 = new byte[] {3, 2, 1, 0}; - Entry entry = database.findEntries("Test attachment").get(0); + E entry = database.findEntries("Test attachment").get(0); assertEquals(1, entry.getBinaryPropertyNames().size()); entry.setBinaryProperty("test", test); assertArrayEquals(test, entry.getBinaryProperty("test")); @@ -153,12 +152,17 @@ public void saveAndReloadCheck() throws IOException { Path file = Files.createTempFile("keepass", "tmp"); saveDatabase(database, getCreds("123".getBytes()), Files.newOutputStream(file)); - Database db = loadDatabase(getCreds("123".getBytes()),Files.newInputStream(file)); - Entry entry = (Entry) db.findEntries("Test attachment").get(0); - assertArrayEquals(new String[] {"letter J.jpeg"}, entry.getBinaryPropertyNames().toArray()); + D db = loadDatabase(getCreds("123".getBytes()),Files.newInputStream(file)); + E newEntry = db.findEntries("Test attachment").get(0); + assertArrayEquals(new String[] {"letter J.jpeg"}, newEntry.getBinaryPropertyNames().toArray()); + E oldEntry = database.findEntries("Test attachment").get(0); + assertArrayEquals(oldEntry.getBinaryProperty("letter J.jpeg"), newEntry.getBinaryProperty("letter J.jpeg")); - entry = (Entry) db.findEntries("Test 2 attachment").get(0); - assertArrayEquals(new String[] {"letter J.jpeg", "letter L.jpeg"}, entry.getBinaryPropertyNames().toArray()); + newEntry = db.findEntries("Test 2 attachment").get(0); + assertArrayEquals(new String[] {"letter J.jpeg", "letter L.jpeg"}, newEntry.getBinaryPropertyNames().toArray()); + oldEntry = database.findEntries("Test 2 attachment").get(0); + assertArrayEquals(oldEntry.getBinaryProperty("letter J.jpeg"), newEntry.getBinaryProperty("letter J.jpeg")); + assertArrayEquals(oldEntry.getBinaryProperty("letter L.jpeg"), newEntry.getBinaryProperty("letter L.jpeg")); } @@ -168,16 +172,17 @@ public void saveAndReloadCheck() throws IOException { @Test public void createAndSaveCheck() throws IOException { Path file = Files.createTempFile("keepass", "tmp"); - Database database1 = newDatabase(); - Entry entry = database1.newEntry("Test attachment"); + D database1 = newDatabase(); + E entry = database1.newEntry("Test attachment"); database1.getRootGroup().addEntry(entry); - InputStream testfile = getClass().getClassLoader().getResourceAsStream("letter J.jpeg"); - byte [] letterJ = ByteStreams.toByteArray(testfile); + InputStream testFile = getClass().getClassLoader().getResourceAsStream("letter J.jpeg"); + assert testFile != null; + byte [] letterJ = ByteStreams.toByteArray(testFile); entry.setBinaryProperty("letter J.jpeg", letterJ); saveDatabase(database1, getCreds("123".getBytes()), Files.newOutputStream(file)); - Database db = loadDatabase(getCreds("123".getBytes()),Files.newInputStream(file)); - Entry entry1 = (Entry) db.findEntries("Test attachment").get(0); + D db = loadDatabase(getCreds("123".getBytes()),Files.newInputStream(file)); + E entry1 = db.findEntries("Test attachment").get(0); // just one property assertArrayEquals(new String[] {"letter J.jpeg"}, entry1.getBinaryPropertyNames().toArray()); // content is correct From 2fe8914591f1a5b698d7b42469578cd191c5e561 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Thu, 11 May 2023 11:51:30 +0100 Subject: [PATCH 095/228] Significant change related to V4 #39, #13. Databases save with same encryption settings as they were loaded, or V4 defaults if they were not loaded. #36 Corrects corruption of JAXB database on save #42 --- .../java/org/linguafranca/pwdb/Database.java | 5 + .../pwdb/kdbx/dom/DomDatabaseWrapper.java | 83 +++++++++++----- .../kdbx/dom/DomSerializableDatabase.java | 1 + .../pwdb/kdbx/dom/DomDatabaseWrapperTest.java | 4 +- .../pwdb/kdbx/dom/DomSaveAndReloadTest.java | 13 +++ .../pwdb/kdbx/dom/VisitorTest.java | 2 +- .../pwdb/kdbx/SimpleQuickStartTest.java | 8 +- .../pwdb/kdbx/validation/Issue_27_Test.java | 4 +- .../pwdb/kdbx/jaxb/JaxbDatabase.java | 36 ++++--- .../kdbx/jaxb/JaxbSerializableDatabase.java | 66 +++++++++---- .../kdbx/jaxb/JaxbBinaryPropertyV3Test.java | 2 +- .../pwdb/kdbx/jaxb/JaxbSaveAndReloadTest.java | 26 +++-- .../linguafranca/pwdb/kdb/KdbDatabase.java | 5 + .../pwdb/kdbx/simple/SimpleDatabase.java | 40 +++----- .../simple/SimpleSerializableDatabase.java | 7 ++ .../kdbx/simple/SimpleSaveAndReloadTest.java | 12 +++ .../pwdb/checks/SaveAndReloadChecks.java | 94 +++++++++++++------ 17 files changed, 279 insertions(+), 129 deletions(-) diff --git a/database/src/main/java/org/linguafranca/pwdb/Database.java b/database/src/main/java/org/linguafranca/pwdb/Database.java index dddac89b..eb867bdf 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Database.java +++ b/database/src/main/java/org/linguafranca/pwdb/Database.java @@ -241,6 +241,11 @@ public interface Database , G extends Group void save(StreamFormat streamFormat, Credentials credentials, OutputStream outputStream) throws IOException; + /** + * Get the format the database was loaded from + */ + StreamFormat getStreamFormat(); + /** * Properties to encrypt * @param propertyName the property of interest diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java index b783bbf7..3a0e5cba 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java @@ -17,12 +17,13 @@ package org.linguafranca.pwdb.kdbx.dom; import org.jetbrains.annotations.NotNull; +import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.StreamConfiguration; +import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.base.AbstractDatabase; import org.linguafranca.pwdb.kdbx.Helpers; +import org.linguafranca.pwdb.kdbx.KdbxHeader; import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; -import org.linguafranca.pwdb.StreamFormat; -import org.linguafranca.pwdb.Credentials; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -31,6 +32,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.Objects; import java.util.UUID; import static com.google.common.base.Preconditions.checkNotNull; @@ -42,36 +44,48 @@ * * @author jo */ -public class DomDatabaseWrapper extends AbstractDatabase { +public class DomDatabaseWrapper extends AbstractDatabase { + private final DomSerializableDatabase domDatabase = DomSerializableDatabase.createEmptyDatabase(); + Element dbMeta; private Document document; private Element dbRootGroup; - Element dbMeta; - private final DomSerializableDatabase domDatabase = DomSerializableDatabase.createEmptyDatabase(); + private StreamFormat streamFormat; - public DomDatabaseWrapper () { + + public DomDatabaseWrapper() { init(); } - public DomDatabaseWrapper (StreamFormat streamFormat, Credentials credentials, InputStream inputStream) throws IOException { + /** + * load a database + * @param credentials credentials to use + * @param inputStream where to read from + * @return a database + */ + public DomDatabaseWrapper(Credentials credentials, InputStream inputStream) throws IOException { + this.streamFormat = new KdbxStreamFormat(); streamFormat.load(domDatabase, credentials, inputStream); init(); } - public static DomDatabaseWrapper load (@NotNull Credentials credentials, @NotNull InputStream inputStream) throws IOException { - return new DomDatabaseWrapper(new KdbxStreamFormat(), + /** + * load a database + * @param credentials credentials to use + * @param inputStream where to read from + * @return a database + */ + public static DomDatabaseWrapper load(@NotNull Credentials credentials, + @NotNull InputStream inputStream) throws IOException { + return new DomDatabaseWrapper( checkNotNull(credentials, "Credentials must not be null"), checkNotNull(inputStream, "InputStream must not be null")); } - public static DomDatabaseWrapper load (StreamFormat streamFormat, @NotNull Credentials credentials, @NotNull InputStream inputStream) throws IOException { - return new DomDatabaseWrapper(streamFormat, credentials, inputStream); - } - - - private void init() { + private void init() { document = domDatabase.getDoc(); try { dbRootGroup = ((Element) DomHelper.xpath.evaluate("/KeePassFile/Root/Group", document, XPathConstants.NODE)); @@ -81,13 +95,29 @@ private void init() { } } + /** + * Save database with same format/encryption settings as it was loaded with or V4 defaults if was not loaded + * @param credentials credentials to use + * @param outputStream where to write + */ @Override - public void save(Credentials credentials, OutputStream outputStream) throws IOException { - new KdbxStreamFormat().save(domDatabase, credentials, outputStream); - setDirty(false); - } - - public void save(StreamFormat streamFormat, Credentials credentials, OutputStream outputStream) throws IOException { + public void save(Credentials credentials, + OutputStream outputStream) throws IOException { + if (Objects.isNull(streamFormat)){ + streamFormat = new KdbxStreamFormat(new KdbxHeader(4)); + } + save(streamFormat, credentials, outputStream); + } + + /** + * Save database with same format/encryption settings as it was loaded with + * @param streamFormat format/config to use for saving + * @param credentials credentials to use + * @param outputStream where to write + */ + public void save(StreamFormat streamFormat, + Credentials credentials, + OutputStream outputStream) throws IOException { streamFormat.save(domDatabase, credentials, outputStream); setDirty(false); } @@ -122,7 +152,7 @@ public DomIconWrapper newIcon() { @Override public DomIconWrapper newIcon(Integer i) { - DomIconWrapper icon = newIcon(); + DomIconWrapper icon = newIcon(); icon.setIndex(i); return icon; } @@ -130,10 +160,10 @@ public DomIconWrapper newIcon(Integer i) { @Override public DomGroupWrapper getRecycleBin() { String UuidContent = getElementContent(RECYCLE_BIN_UUID_ELEMENT_NAME, dbMeta); - if (UuidContent != null){ + if (UuidContent != null) { final UUID uuid = Helpers.uuidFromBase64(UuidContent); if (uuid.getLeastSignificantBits() != 0 && uuid.getMostSignificantBits() != 0) { - for (DomGroupWrapper g: getRootGroup().getGroups()) { + for (DomGroupWrapper g : getRootGroup().getGroups()) { if (g.getUuid().equals(uuid)) { return g; } @@ -186,6 +216,7 @@ public void setDescription(String description) { DomHelper.touchElement("DatabaseDescriptionChanged", dbMeta); setDirty(true); } - - + public StreamFormat getStreamFormat() { + return streamFormat; + } } diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java index 5663cae4..0e313952 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java @@ -123,6 +123,7 @@ public int getBinaryCount() { @Override public void save(OutputStream outputStream) { + // make a copy so we can mess with content Document copyDoc = (Document) doc.cloneNode(true); try { // check whether protection is required and if so mark the element with @Protected='True' diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java index e6c20a0d..ade42884 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java @@ -38,7 +38,7 @@ public DomDatabaseWrapperTest() throws IOException { @Test public void inspectPasswordDatabase() throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test123.kdbx"); - DomDatabaseWrapper database = new DomDatabaseWrapper(new KdbxStreamFormat(), new KdbxCreds("123".getBytes()), inputStream); + DomDatabaseWrapper database = new DomDatabaseWrapper(new KdbxCreds("123".getBytes()), inputStream); database.save(new StreamFormat.None(), new Credentials.None(), System.out); } @@ -48,7 +48,7 @@ public void inspectKeyfileDatabase() throws IOException { InputStream keyFileInputStream = getClass().getClassLoader().getResourceAsStream("KeyFileDatabase.key"); Credentials credentials = new KdbxCreds("123".getBytes(), keyFileInputStream); InputStream inputStream = getClass().getClassLoader().getResourceAsStream("KeyFileDatabase.kdbx"); - DomDatabaseWrapper database = new DomDatabaseWrapper(new KdbxStreamFormat(), credentials, inputStream); + DomDatabaseWrapper database = new DomDatabaseWrapper(credentials, inputStream); database.save(new StreamFormat.None(), new Credentials.None(), System.out); } diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomSaveAndReloadTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomSaveAndReloadTest.java index a7a75ba5..704f0a72 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomSaveAndReloadTest.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomSaveAndReloadTest.java @@ -16,10 +16,13 @@ package org.linguafranca.pwdb.kdbx.dom; +import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.checks.SaveAndReloadChecks; import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.kdbx.KdbxHeader; +import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import java.io.IOException; import java.io.InputStream; @@ -54,4 +57,14 @@ public Database loadDatabase(Credentials credentials, InputStream inputStream) t public Credentials getCreds(byte[] creds) { return new KdbxCreds("123".getBytes()); } + + @Override + public boolean verifyStreamFormat(StreamFormat s1, StreamFormat s2) { + KdbxHeader h1 = (KdbxHeader) s1.getStreamConfiguration(); + KdbxHeader h2 = (KdbxHeader) s1.getStreamConfiguration(); + return (h1.getVersion() == h2.getVersion() && + h1.getProtectedStreamAlgorithm().equals(h2.getProtectedStreamAlgorithm()) && + h1.getKeyDerivationFunction().equals(h2.getKeyDerivationFunction()) && + h1.getCipherAlgorithm().equals(h2.getCipherAlgorithm())); + } } diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/VisitorTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/VisitorTest.java index 4c908349..eb655678 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/VisitorTest.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/VisitorTest.java @@ -64,7 +64,7 @@ public void visit(Entry entry) { public void testLoadDB() { try { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test123.kdbx"); - DomDatabaseWrapper db = new DomDatabaseWrapper(new KdbxStreamFormat(), new KdbxCreds("123".getBytes()), inputStream); + DomDatabaseWrapper db = new DomDatabaseWrapper(new KdbxCreds("123".getBytes()), inputStream); db.visit(visitor); //noinspection unchecked diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/SimpleQuickStartTest.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/SimpleQuickStartTest.java index c8a36c0f..f9c8a71f 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/SimpleQuickStartTest.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/SimpleQuickStartTest.java @@ -76,8 +76,8 @@ public void loadSave() throws IOException { loadKdbx3SaveKdbx4("test123.kdbx","123".getBytes(), Files.newOutputStream(path)); // load newly created V4 database - KdbxStreamFormat streamFormat = new KdbxStreamFormat(); - DomDatabaseWrapper.load(streamFormat, new KdbxCreds("123".getBytes()), Files.newInputStream(path)); + DomDatabaseWrapper db = DomDatabaseWrapper.load(new KdbxCreds("123".getBytes()), Files.newInputStream(path)); + KdbxStreamFormat streamFormat = (KdbxStreamFormat) db.getStreamFormat(); assertEquals(4, streamFormat.getStreamConfiguration().getVersion()); assertEquals(Encryption.Cipher.CHA_CHA_20, streamFormat.getStreamConfiguration().getCipherAlgorithm()); assertEquals(Encryption.KeyDerivationFunction.ARGON2, streamFormat.getStreamConfiguration().getKeyDerivationFunction()); @@ -90,8 +90,8 @@ public void loadSave2() throws IOException { loadKdbx4SaveKdbx3("V4-ChaCha20-Argon2-Attachment.kdbx","123".getBytes(), Files.newOutputStream(path)); // load newly created V4 database - KdbxStreamFormat streamFormat = new KdbxStreamFormat(); - DomDatabaseWrapper.load(streamFormat, new KdbxCreds("123".getBytes()), Files.newInputStream(path)); + DomDatabaseWrapper db = DomDatabaseWrapper.load(new KdbxCreds("123".getBytes()), Files.newInputStream(path)); + KdbxStreamFormat streamFormat = (KdbxStreamFormat) db.getStreamFormat(); assertEquals(3, streamFormat.getStreamConfiguration().getVersion()); assertEquals(Encryption.Cipher.AES, streamFormat.getStreamConfiguration().getCipherAlgorithm()); assertEquals(Encryption.KeyDerivationFunction.AES, streamFormat.getStreamConfiguration().getKeyDerivationFunction()); diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_27_Test.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_27_Test.java index 30d594ea..56a9ab99 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_27_Test.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_27_Test.java @@ -23,7 +23,7 @@ public class Issue_27_Test { * Check load of problem file */ @Test - public void testIssue27() { + public void testIssue27() throws IOException { InputStream is = this.getClass().getClassLoader().getResourceAsStream("issue-27/bogus-timestamp2.kdbx"); KdbxCreds creds = new KdbxCreds("passwordless".getBytes()); JaxbDatabase db = JaxbDatabase.load(creds, is); @@ -39,7 +39,7 @@ public void testIssue27() { * Verify that V4 dates are still processed correctly */ @Test - public void testV4Date() { + public void testV4Date() throws IOException { InputStream is = this.getClass().getClassLoader().getResourceAsStream("V4-AES-AES.kdbx"); KdbxCreds creds = new KdbxCreds("123".getBytes()); JaxbDatabase db = JaxbDatabase.load(creds, is); diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java index bf471700..619e6a59 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java @@ -21,6 +21,7 @@ import org.linguafranca.pwdb.StreamConfiguration; import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.base.AbstractDatabase; +import org.linguafranca.pwdb.kdbx.KdbxHeader; import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import org.linguafranca.pwdb.kdbx.jaxb.binding.KeePassFile; import org.linguafranca.pwdb.kdbx.jaxb.binding.ObjectFactory; @@ -29,6 +30,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.Date; +import java.util.Objects; import java.util.UUID; /** @@ -42,46 +44,46 @@ public class JaxbDatabase extends AbstractDatabase streamFormat; public JaxbDatabase() { - this(createEmptyDatabase().getKeePassFile()); + this(createEmptyDatabase().getKeePassFile(), null); } - private JaxbDatabase(KeePassFile keePassFile) { + private JaxbDatabase(KeePassFile keePassFile, StreamFormat streamFormat) { this.keePassFile = keePassFile; this.root = new JaxbGroup(this, keePassFile.getRoot().getGroup()); + this.streamFormat = streamFormat; } public static JaxbDatabase createEmptyDatabase() { InputStream inputStream = JaxbDatabase.class.getClassLoader().getResourceAsStream("base.kdbx.xml"); KeePassFile keePassFile = new JaxbSerializableDatabase().load(inputStream).keePassFile; keePassFile.getRoot().getGroup().setUUID(UUID.randomUUID()); - return new JaxbDatabase(keePassFile); + return new JaxbDatabase(keePassFile, null); } - public static JaxbDatabase load(Credentials creds, InputStream inputStream) { + public static JaxbDatabase load(Credentials creds, InputStream inputStream) throws IOException { return load(new KdbxStreamFormat(), creds, inputStream); } @NotNull - public static JaxbDatabase load(StreamFormat format, Credentials creds, InputStream inputStream) { + public static JaxbDatabase load(StreamFormat format, Credentials creds, InputStream inputStream) throws IOException { JaxbSerializableDatabase db = new JaxbSerializableDatabase(); - try { - format.load(db, creds, inputStream); - } catch (IOException e) { - throw new IllegalStateException(e); - } - return new JaxbDatabase(db.getKeePassFile()); + format.load(db, creds, inputStream); + return new JaxbDatabase(db.getKeePassFile(), format); } @Override public void save(Credentials creds, OutputStream outputStream) throws IOException { - save(new KdbxStreamFormat(), creds, outputStream); + if (Objects.isNull(this.streamFormat)){ + this.streamFormat = new KdbxStreamFormat(new KdbxHeader(4)); + } + save(this.streamFormat, creds, outputStream); } public void save(StreamFormat format, Credentials creds, OutputStream outputStream) throws IOException { - JaxbSerializableDatabase jsd = new JaxbSerializableDatabase(); - jsd.setKeePassFile(this.keePassFile); + JaxbSerializableDatabase jsd = new JaxbSerializableDatabase(this.keePassFile); format.save(jsd, creds, outputStream); setDirty(false); } @@ -187,4 +189,10 @@ ObjectFactory getObjectFactory() { public void createBinary(byte[] value, Integer index) { JaxbSerializableDatabase.addBinary(getKeePassFile(), getObjectFactory(), index, value); } + + @SuppressWarnings("unchecked") + @Override + public StreamFormat getStreamFormat(){ + return streamFormat; + } } diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java index e228a7ce..5a83fcf2 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java @@ -32,6 +32,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * @author jo @@ -39,10 +40,29 @@ @SuppressWarnings("WeakerAccess") public class JaxbSerializableDatabase implements SerializableDatabase { + private final ObjectFactory objectFactory = new ObjectFactory(); protected KeePassFile keePassFile; private StreamEncryptor encryption; - private final ObjectFactory objectFactory = new ObjectFactory(); + public JaxbSerializableDatabase() { + + } + + public JaxbSerializableDatabase(KeePassFile keePassFile) { + this.keePassFile = keePassFile; + } + + public static void addBinary(KeePassFile keePassFile, ObjectFactory objectFactory, int index, byte[] value) { + // create a new binary to put in the store + Binaries.Binary newBin = objectFactory.createBinariesBinary(); + newBin.setID(index); + newBin.setValue(Helpers.zipBinaryContent(value)); + newBin.setCompressed(true); + if (keePassFile.getMeta().getBinaries() == null) { + keePassFile.getMeta().setBinaries(objectFactory.createBinaries()); + } + keePassFile.getMeta().getBinaries().getBinary().add(newBin); + } @Override public JaxbSerializableDatabase load(InputStream inputStream) { @@ -54,7 +74,7 @@ public JaxbSerializableDatabase load(InputStream inputStream) { public void afterUnmarshal(Object target, Object parent) { if (target instanceof StringField.Value) { StringField.Value value = (StringField.Value) target; - if (value.getProtected() !=null && value.getProtected()) { + if (value.getProtected() != null && value.getProtected()) { byte[] encrypted = Base64.decodeBase64(value.getValue().getBytes()); String decrypted = new String(encryption.decrypt(encrypted), StandardCharsets.UTF_8); value.setValue(decrypted); @@ -98,12 +118,21 @@ public void save(OutputStream outputStream) throws IOException { JAXBContext jc = JAXBContext.newInstance(KeePassFile.class); Marshaller u = jc.createMarshaller(); u.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + // we encrypt values on marshal and then reset them afterwards + // this may seem a bit clunky, but seems like it's actually less fiddly than other things u.setListener(new Marshaller.Listener() { + String savedValue = ""; + + /** + * Change protected fields on marshal + * @param source instance of JAXB mapped class prior to marshalling from it. + */ @Override public void beforeMarshal(Object source) { if (source instanceof StringField) { StringField field = (StringField) source; if (toEncrypt.contains(field.getKey())) { + this.savedValue = field.getValue().getValue(); byte[] encrypted = encryption.encrypt(field.getValue().getValue().getBytes()); String b64 = new String(Base64.encodeBase64(encrypted), StandardCharsets.UTF_8); field.getValue().setValue(b64); @@ -111,6 +140,22 @@ public void beforeMarshal(Object source) { } } } + + /** + * Restore protected fields after marshal + * @param source instance of JAXB mapped class after marshalling it. + */ + @Override + public void afterMarshal(Object source) { + if (source instanceof StringField) { + StringField field = (StringField) source; + if (Objects.nonNull(field.getValue().getProtected()) && field.getValue().getProtected()) { + field.getValue().setValue(savedValue ); + field.getValue().setProtected(false); + savedValue = ""; + } + } + } }); u.marshal(keePassFile, outputStream); } catch (JAXBException e) { @@ -150,23 +195,12 @@ public byte[] getBinary(int index) { @Override public int getBinaryCount() { - return keePassFile.getMeta().getBinaries().getBinary().size(); - } - - public static void addBinary(KeePassFile keePassFile, ObjectFactory objectFactory, int index, byte[] value) { - // create a new binary to put in the store - Binaries.Binary newBin = objectFactory.createBinariesBinary(); - newBin.setID(index); - newBin.setValue(Helpers.zipBinaryContent(value)); - newBin.setCompressed(true); - if (keePassFile.getMeta().getBinaries() == null) { - keePassFile.getMeta().setBinaries(objectFactory.createBinaries()); + if (Objects.isNull(keePassFile.getMeta().getBinaries())) { + return 0; } - keePassFile.getMeta().getBinaries().getBinary().add(newBin); + return keePassFile.getMeta().getBinaries().getBinary().size(); } - - public KeePassFile getKeePassFile() { return keePassFile; } diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV3Test.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV3Test.java index 802703cf..d014cc9b 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV3Test.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV3Test.java @@ -30,7 +30,7 @@ */ public class JaxbBinaryPropertyV3Test extends BinaryPropertyChecks { - public JaxbBinaryPropertyV3Test() { + public JaxbBinaryPropertyV3Test() throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment.kdbx"); database = JaxbDatabase.load(new KdbxCreds("123".getBytes()),inputStream); } diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSaveAndReloadTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSaveAndReloadTest.java index 0c3680e6..37eb4341 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSaveAndReloadTest.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSaveAndReloadTest.java @@ -16,10 +16,11 @@ package org.linguafranca.pwdb.kdbx.jaxb; -import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.checks.SaveAndReloadChecks; import org.linguafranca.pwdb.kdbx.KdbxCreds; -import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.kdbx.KdbxHeader; import java.io.IOException; import java.io.InputStream; @@ -28,24 +29,24 @@ /** * @author jo */ -public class JaxbSaveAndReloadTest extends SaveAndReloadChecks { +public class JaxbSaveAndReloadTest extends SaveAndReloadChecks { @Override - public Database getDatabase() { + public JaxbDatabase getDatabase() { return new JaxbDatabase(); } @Override - public Database getDatabase(String name, Credentials credentials) { + public JaxbDatabase getDatabase(String name, Credentials credentials) throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream(name); return JaxbDatabase.load(credentials, inputStream); } @Override - public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { + public void saveDatabase(JaxbDatabase database, Credentials credentials, OutputStream outputStream) throws IOException { database.save(credentials, outputStream); } @Override - public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + public JaxbDatabase loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { return JaxbDatabase.load(credentials, inputStream); } @@ -53,4 +54,15 @@ public Database loadDatabase(Credentials credentials, InputStream inputStream) t public Credentials getCreds(byte[] creds) { return new KdbxCreds(creds); } + + + @Override + public boolean verifyStreamFormat(StreamFormat s1, StreamFormat s2) { + KdbxHeader h1 = (KdbxHeader) s1.getStreamConfiguration(); + KdbxHeader h2 = (KdbxHeader) s1.getStreamConfiguration(); + return (h1.getVersion() == h2.getVersion() && + h1.getProtectedStreamAlgorithm().equals(h2.getProtectedStreamAlgorithm()) && + h1.getKeyDerivationFunction().equals(h2.getKeyDerivationFunction()) && + h1.getCipherAlgorithm().equals(h2.getCipherAlgorithm())); + } } diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbDatabase.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbDatabase.java index a9ca71ed..fbb8634e 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbDatabase.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbDatabase.java @@ -174,4 +174,9 @@ public boolean supportsBinaryProperties() { public boolean supportsRecycleBin() { return false; } + + @Override + public StreamFormat getStreamFormat(){ + return null; + } } diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java index 90c1f956..0f42e849 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java @@ -45,21 +45,19 @@ public class SimpleDatabase extends AbstractDatabase streamFormat; - private SimpleSerializableDatabase serializableDatabase; /** * Create a new empty database */ public SimpleDatabase() { - this(createEmptyDatabase()); + this(createEmptyDatabase(), null); } - public SimpleDatabase(KeePassFile file) { + public SimpleDatabase(KeePassFile file, StreamFormat streamFormat) { try { keePassFile = file; keePassFile.root.group.database = this; - this.serializableDatabase = new SimpleSerializableDatabase(); - this.serializableDatabase.setKeePassFile(this.keePassFile); + this.streamFormat = streamFormat; SimpleSerializableDatabase.fixUp(keePassFile.root.group); } catch (Exception e) { throw new IllegalStateException(e); @@ -75,38 +73,21 @@ public SimpleDatabase(KeePassFile file) { public static SimpleDatabase loadXml(InputStream inputStream) throws Exception { KeePassFile result = getSerializer().read(KeePassFile.class, inputStream); result.root.group.uuid = UUID.randomUUID(); - return new SimpleDatabase(result); + return new SimpleDatabase(result, null); } /** * Load kdbx file * - * @param credentials the credentials to use - * @param inputStream the encrypted input stream - * @return a new database - */ - public static SimpleDatabase load(Credentials credentials, InputStream inputStream) throws IOException { - // doesn't matter how streamFormat is initialised - return load(new KdbxStreamFormat(), credentials, inputStream); - } - - /** - * Load file with a choice of StreamFormat - * @param streamFormat which contains format information about the file after load * @param credentials credentials to use * @param inputStream where to load from * @return a new database - * @param config for the streamFormat */ - public static SimpleDatabase load(StreamFormat streamFormat, - Credentials credentials, - InputStream inputStream) throws IOException { + public static SimpleDatabase load(Credentials credentials, InputStream inputStream) throws IOException { SimpleSerializableDatabase simpleSerializableDatabase = new SimpleSerializableDatabase(); + StreamFormat streamFormat = new KdbxStreamFormat(); streamFormat.load(simpleSerializableDatabase, credentials, inputStream); - SimpleDatabase db = new SimpleDatabase(simpleSerializableDatabase.getKeePassFile()); - db.serializableDatabase = simpleSerializableDatabase; - db.streamFormat = streamFormat; - return db; + return new SimpleDatabase(simpleSerializableDatabase.getKeePassFile(), streamFormat); } /** @@ -132,7 +113,8 @@ public void save(Credentials credentials, OutputStream outputStream) throws IOEx @Override public void save(StreamFormat streamFormat, Credentials credentials, OutputStream outputStream) throws IOException{ - streamFormat.save(this.serializableDatabase, credentials, outputStream); + SimpleSerializableDatabase simpleSerializableDatabase = new SimpleSerializableDatabase(this.keePassFile); + streamFormat.save(simpleSerializableDatabase, credentials, outputStream); setDirty(false); } @@ -224,4 +206,8 @@ public List getBinaries() { public void addBinary(byte [] bytes, int index) { SimpleSerializableDatabase.addBinary(this.keePassFile, index, bytes); } + + public StreamFormat getStreamFormat() { + return streamFormat; + } } \ No newline at end of file diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleSerializableDatabase.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleSerializableDatabase.java index a3d1ee61..5f669a13 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleSerializableDatabase.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleSerializableDatabase.java @@ -47,6 +47,13 @@ public class SimpleSerializableDatabase implements SerializableDatabase { public KeePassFile keePassFile; private StreamEncryptor encryption; + public SimpleSerializableDatabase(){ + + } + public SimpleSerializableDatabase(KeePassFile keePassFile) { + this.keePassFile = keePassFile; + } + /** * Create an empty underlying KeePassFile instance diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java index e9c80e36..a936180b 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java @@ -17,6 +17,7 @@ package org.linguafranca.pwdb.kdbx.simple; import org.junit.Test; +import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.checks.SaveAndReloadChecks; import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.kdbx.KdbxCreds; @@ -62,6 +63,17 @@ public Database loadDatabase(Credentials credentials, InputStream inputStream) t } + @Override + public boolean verifyStreamFormat(StreamFormat s1, StreamFormat s2) { + KdbxHeader h1 = (KdbxHeader) s1.getStreamConfiguration(); + KdbxHeader h2 = (KdbxHeader) s1.getStreamConfiguration(); + return (h1.getVersion() == h2.getVersion() && + h1.getProtectedStreamAlgorithm().equals(h2.getProtectedStreamAlgorithm()) && + h1.getKeyDerivationFunction().equals(h2.getKeyDerivationFunction()) && + h1.getCipherAlgorithm().equals(h2.getCipherAlgorithm())); + } + + // check that boolean comes out in upper case - Simple Converters don't work on attributes // so this is done in the output transformer @Test diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java index ef42c037..b27a3732 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java @@ -40,59 +40,55 @@ public abstract class SaveAndReloadChecks , G ext public abstract void saveDatabase(D database, Credentials credentials, OutputStream outputStream) throws IOException; public abstract D loadDatabase(Credentials credentials, InputStream inputStream) throws IOException; public abstract Credentials getCreds(byte[] creds); + public abstract boolean verifyStreamFormat (StreamFormat s1, StreamFormat s2); @BeforeClass public static void ensureOutputDir() throws IOException { Files.createDirectories(Paths.get("testOutput")); } + /** + * Test verifies that entries contain the same content on reload as they did on save, + * and also verifies that saving doesn't alter the contents + */ @Test public void saveAndReloadTest() throws IOException { long now = System.currentTimeMillis(); + // create database with known content D output = createNewDatabase(); - Assert.assertTrue(output.isDirty()); - assertEquals(5, output.getRootGroup().getGroupsCount()); + verifyContents(output); + //output.save(new StreamFormat.None(), new Credentials.None(), System.out); FileOutputStream fos = new FileOutputStream("testOutput/test1.kdbx"); saveDatabase(output, getCreds("123".getBytes()), fos); Assert.assertFalse(output.isDirty()); fos.flush(); fos.close(); + // make sure that saving didn't mess up content + verifyContents(output); + //output.save(new StreamFormat.None(), new Credentials.None(), System.out); + FileInputStream fis = new FileInputStream("testOutput/test1.kdbx"); D input = loadDatabase(getCreds("123".getBytes()), fis); - - for (Integer g = 0; g< 5; g++){ - Group group = input.getRootGroup().getGroups().get(g); - assertEquals(g.toString(), group.getName()); - assertEquals(g + 1, group.getEntries().size()); - assertEquals(g+1, group.getEntriesCount()); - assertEquals(input.getRootGroup(), group.getParent()); - for (int e = 0; e <= g; e++) { - Entry entry = (Entry) group.getEntries().get(e); - assertEquals(g + "-" + e, entry.getTitle()); - assertEquals(g + " - un - " + e, entry.getUsername()); - assertEquals(g + "- p -" + e, entry.getPassword()); - assertEquals(g + "- url - " + e, entry.getUrl()); - assertEquals(g + "- n - " + e, entry.getNotes()); - assertEquals(group, entry.getParent()); - } - } + verifyContents(input); //saveDatabase(input, new StreamFormat.None(), new Credentials.None(), System.out); System.out.format("Test took %d millis", System.currentTimeMillis() - now); - } + /** + * Test verifies that attachments are saved and reloaded correctly + */ @Test public void saveAndReloadTest2() throws IOException { D attachment = getDatabase("Attachment.kdbx", getCreds("123".getBytes())); - Entry entry = attachment.findEntries("Test attachment").get(0); + E entry = attachment.findEntries("Test attachment").get(0); assertArrayEquals(new String[] {"letter J.jpeg"}, entry.getBinaryPropertyNames().toArray()); - Entry entry2 = attachment.findEntries("Test 2 attachment").get(0); + E entry2 = attachment.findEntries("Test 2 attachment").get(0); assertArrayEquals(new String[] {"letter J.jpeg", "letter L.jpeg"}, entry2.getBinaryPropertyNames().toArray()); byte[] content = entry2.getBinaryProperty("letter L.jpeg"); @@ -105,20 +101,41 @@ public void saveAndReloadTest2() throws IOException { fos.close(); FileInputStream fis = new FileInputStream("testOutput/test2.kdbx"); - Database input = loadDatabase(getCreds("123".getBytes()), fis); + D input = loadDatabase(getCreds("123".getBytes()), fis); - entry = (Entry) input.findEntries("Test attachment").get(0); + entry = input.findEntries("Test attachment").get(0); assertArrayEquals(new String[] {"letter J.jpeg", "letter L.jpeg"}, entry.getBinaryPropertyNames().toArray()); - //saveDatabase(input, new StreamFormat.None(), new Credentials.None(), System.out); + } + + String [] testFiles = {"V4-AES-AES.kdbx", + "V4-AES-Argon2.kdbx", + "V4-ChaCha20-AES.kdbx", + "V4-ChaCha20-Argon2-Attachment.kdbx"}; + /*** + * Test verifies that database is saved with same encryption that it was loaded with + */ + @Test + public void saveAndReloadTest3() throws IOException { + for (String resource: testFiles) { + D database = getDatabase(resource, this.getCreds("123".getBytes())); + StreamFormat format1 = database.getStreamFormat(); + + database.save(getCreds("123".getBytes()), Files.newOutputStream(Paths.get("testOutput/test3.kdbx"))); + + FileInputStream fis = new FileInputStream("testOutput/test3.kdbx"); + D input = loadDatabase(getCreds("123".getBytes()), fis); + StreamFormat format2 = input.getStreamFormat(); + assertTrue(verifyStreamFormat(format1, format2)); + } } private D createNewDatabase() throws IOException { D database = getDatabase(); - for (Integer g = 0; g < 5; g++){ + for (@SuppressWarnings("WrapperTypeMayBePrimitive") Integer g = 0; g < 5; g++){ G group = database.getRootGroup().addGroup(database.newGroup(g.toString())); for (int e = 0; e <= g; e++) { group.addEntry(entryFactory(database, g.toString(), e)); @@ -138,6 +155,25 @@ private E entryFactory(D database, String g, int e) { return result; } + private void verifyContents(D database) { + for (Integer g = 0; g< 5; g++){ + G group = database.getRootGroup().getGroups().get(g); + assertEquals(g.toString(), group.getName()); + assertEquals(g + 1, group.getEntries().size()); + assertEquals(g+1, group.getEntriesCount()); + assertEquals(database.getRootGroup(), group.getParent()); + for (int e = 0; e <= g; e++) { + E entry = group.getEntries().get(e); + assertEquals(g + "-" + e, entry.getTitle()); + assertEquals(g + " - un - " + e, entry.getUsername()); + assertEquals(g + "- p -" + e, entry.getPassword()); + assertEquals(g + "- url - " + e, entry.getUrl()); + assertEquals(g + "- n - " + e, entry.getNotes()); + assertEquals(group, entry.getParent()); + } + } + } + /** * Outputs the database to a file - we can try to read it in other versions of the program. Run "manually". * @@ -155,9 +191,9 @@ public void saveNewDatabase () throws IOException { * Doesn't do anything other than output the database using default PrintVisitor * @throws IOException when naughty */ - @Test + @Test @Ignore public void inspectNewDatabase () throws IOException { - Database database = createNewDatabase(); + D database = createNewDatabase(); database.visit(new Visitor.Print()); } @@ -199,7 +235,7 @@ public void testNewDatabase() throws IOException { root.addGroup(group1); root.removeGroup(group1); - Assert.assertTrue(group1.getParent() == null); + assertNull(group1.getParent()); assertEquals(0, root.getGroups().size()); root.addGroup(group1); assertEquals(1, root.getGroups().size()); From d0353b72c7c172aa2a03c71bd109f6ab7be551a2 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 12 May 2023 16:26:54 +0100 Subject: [PATCH 096/228] Provide a "inhibitConsoleOutput" Maven profile and add MavenRunListener #45 --- .../java/org/linguafranca/pwdb/Visitor.java | 43 +++++++---- .../pwdb/kdbx/dom/DomDatabaseWrapperTest.java | 9 ++- .../pwdb/kdbx/dom/DomHelperTest.java | 9 ++- .../linguafranca/pwdb/kdbx/dom/DomV4Test.java | 16 ++-- .../pwdb/kdbx/dom/VisitorTest.java | 9 ++- .../linguafranca/pwdb/kdbx/OpenDbExample.java | 15 ++-- .../linguafranca/pwdb/kdbx/QuickStart.java | 6 +- .../org/linguafranca/pwdb/kdbx/SaxParse.java | 75 +++++++------------ .../java/org/linguafranca/pwdb/kdbx/Util.java | 19 +++-- .../linguafranca/pwdb/kdbx/SaxParseTest.java | 14 +++- .../org/linguafranca/pwdb/kdbx/UtilTest.java | 22 +++++- .../pwdb/kdbx/validation/Issue_27_Test.java | 9 ++- .../pwdb/kdbx/validation/Issue_38_Test.java | 6 +- .../jaxb/JaxbSerializableDatabaseTest.java | 10 ++- .../linguafranca/pwdb/kdbx/jaxb/JaxbTest.java | 10 ++- .../pwdb/kdbx/jaxb/JaxbV4LoadTest.java | 10 ++- .../pwdb/kdb/KdbSerializerTest.java | 8 +- .../pwdb/hashedblock/HashBlockTest.java | 4 +- .../linguafranca/pwdb/kdbx/HelperTest.java | 14 ++-- .../pwdb/kdbx/KdbxHeaderTest.java | 7 +- .../pwdb/kdbx/KdbxKeyFileTest.java | 7 +- .../pwdb/kdbx/KdbxSerializerTest.java | 7 +- .../pwdb/security/StreamEncryptionTest.java | 8 +- logback.xml | 21 ++++++ pom.xml | 46 ++++++++++-- .../pwdb/kdbx/KdbxInnerInputStreamTest.java | 6 +- .../kdbx/simple/SimpleDatabaseLoadTest.java | 17 +++-- .../kdbx/simple/SimpleSaveAndReloadTest.java | 6 +- test/pom.xml | 16 ---- .../pwdb/checks/DatabaseLoaderChecks.java | 13 +++- .../pwdb/checks/SaveAndReloadChecks.java | 15 ++-- .../java/org/linguafranca/util/HexViewer.java | 12 ++- .../linguafranca/util/MavenRunListener.java | 55 ++++++++++++++ .../java/org/linguafranca/util/TestUtil.java | 17 +++++ 34 files changed, 401 insertions(+), 160 deletions(-) create mode 100644 logback.xml create mode 100644 test/src/main/java/org/linguafranca/util/MavenRunListener.java create mode 100644 test/src/main/java/org/linguafranca/util/TestUtil.java diff --git a/database/src/main/java/org/linguafranca/pwdb/Visitor.java b/database/src/main/java/org/linguafranca/pwdb/Visitor.java index ac3493c2..6cfa57d8 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Visitor.java +++ b/database/src/main/java/org/linguafranca/pwdb/Visitor.java @@ -16,29 +16,31 @@ package org.linguafranca.pwdb; +import java.io.PrintStream; + /** * Interface for implementing a visitor for Groups, their sub-Groups and their Entries. * * @author jo */ -public interface Visitor { +public interface Visitor , G extends Group, E extends Entry, I extends Icon> { /** * Called on entry to a group visit * @param group the group being visited */ - void startVisit(Group group); + void startVisit(G group); /** * Called on exit from a group visit * @param group the group being exited */ - void endVisit(Group group); + void endVisit(G group); /** * Called on visit to an entry * @param entry the entry being visited */ - void visit(Entry entry); + void visit(E entry); /** * called to determine whether to visit entries before subgroups, or not @@ -49,16 +51,17 @@ public interface Visitor { /** * Empty implementation of Visitor */ - abstract class Default implements Visitor { + abstract class Default , G extends Group, E extends Entry, I extends Icon> + implements Visitor { @Override - public void startVisit(Group group) {} + public void startVisit(G group) {} @Override - public void endVisit(Group group) {} + public void endVisit(G group) {} @Override - public void visit(Entry entry) {} + public void visit(E entry) {} @Override public boolean isEntriesFirst() { @@ -69,15 +72,29 @@ public boolean isEntriesFirst() { /** * Visitor prints the Groups and Entries it visits to console */ - class Print extends Default { + class Print , G extends Group, E extends Entry, I extends Icon> + extends Default { + + private final PrintStream printStream; + + public Print() { + this(System.out); + } + + public Print(PrintStream out) { + this.printStream = out; + } + @Override - public void startVisit(Group group) { - System.out.println(group.toString()); + public void startVisit(G group) { + printStream.println(group.toString()); } @Override - public void visit(Entry entry) { - System.out.println(entry.toString()); + @Deprecated + public void visit(E entry) { + printStream.println(entry.toString()); } + } } diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java index ade42884..aef78d3d 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java @@ -26,12 +26,17 @@ import java.io.IOException; import java.io.InputStream; +import java.io.PrintStream; + +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * @author jo */ public class DomDatabaseWrapperTest extends BasicDatabaseChecks { + static PrintStream printStream = getTestPrintStream(); + public DomDatabaseWrapperTest() throws IOException { } @@ -40,7 +45,7 @@ public void inspectPasswordDatabase() throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test123.kdbx"); DomDatabaseWrapper database = new DomDatabaseWrapper(new KdbxCreds("123".getBytes()), inputStream); - database.save(new StreamFormat.None(), new Credentials.None(), System.out); + database.save(new StreamFormat.None(), new Credentials.None(), printStream); } @Test @@ -50,7 +55,7 @@ public void inspectKeyfileDatabase() throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("KeyFileDatabase.kdbx"); DomDatabaseWrapper database = new DomDatabaseWrapper(credentials, inputStream); - database.save(new StreamFormat.None(), new Credentials.None(), System.out); + database.save(new StreamFormat.None(), new Credentials.None(), printStream); } @Override diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomHelperTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomHelperTest.java index 2863983e..227b5e3e 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomHelperTest.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomHelperTest.java @@ -19,19 +19,24 @@ import org.junit.Test; import org.linguafranca.pwdb.kdbx.Helpers; +import java.io.PrintStream; + import static org.junit.Assert.assertEquals; +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * @author jo */ public class DomHelperTest { + static PrintStream printStream = getTestPrintStream(); + @Test public void testBase64RandomUuid() throws Exception { // just check that it can do something String uuid1 = DomHelper.base64RandomUuid(); - System.out.println(Helpers.uuidFromBase64(uuid1)); + printStream.println(Helpers.uuidFromBase64(uuid1)); String uuid2 = DomHelper.base64RandomUuid(); - System.out.println(Helpers.uuidFromBase64(uuid2)); + printStream.println(Helpers.uuidFromBase64(uuid2)); } } \ No newline at end of file diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomV4Test.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomV4Test.java index ae78365d..d3ad03a2 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomV4Test.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomV4Test.java @@ -1,26 +1,32 @@ package org.linguafranca.pwdb.kdbx.dom; import org.junit.Test; -import org.linguafranca.pwdb.Entry; import org.linguafranca.pwdb.Visitor; import org.linguafranca.pwdb.kdbx.KdbxCreds; import java.io.InputStream; +import java.io.PrintStream; + +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * @author jo */ public class DomV4Test { + + static PrintStream printStream = getTestPrintStream(); + @Test public void loadKdbxV4() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); DomDatabaseWrapper database = DomDatabaseWrapper.load(new KdbxCreds("123".getBytes()), inputStream); - database.visit(new Visitor.Print()); + database.visit(new Visitor.Print(printStream)); // test what happens to dates in V4 - database.visit(new Visitor.Default(){ + database.visit(new Visitor.Default(){ + @Override - public void visit(Entry entry) { - System.out.println(entry.getCreationTime()); + public void visit(DomEntryWrapper entry) { + printStream.println(entry.getCreationTime()); } }); } diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/VisitorTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/VisitorTest.java index eb655678..199c78b9 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/VisitorTest.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/VisitorTest.java @@ -25,21 +25,26 @@ import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import java.io.InputStream; +import java.io.PrintStream; import java.util.ArrayList; import java.util.List; import static org.junit.Assert.assertTrue; +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * @author Jo */ public class VisitorTest { + + static PrintStream printStream = getTestPrintStream(); + private List visitorList = new ArrayList<>(); private Visitor visitor = new Visitor() { StringBuffer indentation = new StringBuffer(); @Override public void startVisit(Group group) { - System.out.println(indentation.toString() + group.getName()); + printStream.println(indentation.toString() + group.getName()); indentation.append(" "); } @@ -55,7 +60,7 @@ public boolean isEntriesFirst() { @Override public void visit(Entry entry) { - System.out.println(indentation.toString() + "= " + entry.getTitle()); + printStream.println(indentation.toString() + "= " + entry.getTitle()); visitorList.add(entry); } }; diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java index 772d6a8e..8f2b31f9 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java @@ -7,6 +7,9 @@ import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; import java.io.InputStream; +import java.io.PrintStream; + +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * Example and naive speed test for various implementations. @@ -15,6 +18,8 @@ */ public class OpenDbExample { + static PrintStream printStream = getTestPrintStream(); + private interface DbLoader { Database load(KdbxCreds creds, InputStream inputStream) throws Exception; } @@ -52,17 +57,17 @@ public static void testDb (DbLoader loader, String label, int loads, int iterati } } } - System.out.printf("%s %d loads %d iterations %d millis%n", label, loads, iterations, System.currentTimeMillis()-start); + printStream.printf("%s %d loads %d iterations %d millis%n", label, loads, iterations, System.currentTimeMillis()-start); } public static void main(String[] args) throws Exception { - System.out.println("Warming up JVM"); + printStream.println("Warming up JVM"); testDb(new SimpleDbLoader(), "Simple", 5, 20); testDb(new JaxbDbLoader(), "Jaxb", 5, 20); testDb(new DomDbLoader(), "Dom", 5, 20); - System.out.println("Sleeping"); + printStream.println("Sleeping"); System.gc(); Thread.sleep(2000); @@ -70,7 +75,7 @@ public static void main(String[] args) throws Exception { testDb(new JaxbDbLoader(), "Jaxb", 5, 20); testDb(new DomDbLoader(), "Dom", 5, 20); - System.out.println("Sleeping"); + printStream.println("Sleeping"); System.gc(); Thread.sleep(2000); @@ -78,7 +83,7 @@ public static void main(String[] args) throws Exception { testDb(new JaxbDbLoader(), "Jaxb", 10, 1); testDb(new DomDbLoader(), "Dom", 10, 1); - System.out.println("Sleeping"); + printStream.println("Sleeping"); System.gc(); Thread.sleep(2000); diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java index 6aceb311..9ea05152 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java @@ -29,6 +29,7 @@ import java.util.List; import static org.junit.Assert.assertEquals; +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * Examples for QuickStart @@ -38,6 +39,9 @@ @SuppressWarnings("WeakerAccess") public abstract class QuickStart, G extends Group, E extends Entry, I extends Icon> { + static PrintStream printStream = getTestPrintStream(); + + public abstract D getDatabase(); public abstract D loadDatabase(Credentials creds, InputStream inputStream); @@ -57,7 +61,7 @@ public void loadKdbx() throws IOException { // Jaxb implementation seems a lot faster than the DOM implementation D database = loadDatabase(credentials, inputStream); // visit all groups and entries and list them to console - database.visit(new Visitor.Print()); + database.visit(new Visitor.Print(printStream)); } } diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java index 96ab4efb..2128b9d2 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java @@ -19,12 +19,14 @@ import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.security.StreamEncryptor; import org.xml.sax.*; +import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import java.io.IOException; import java.io.InputStream; +import java.io.PrintWriter; /** * @author jo @@ -32,11 +34,12 @@ public class SaxParse { /** * SAX Parsing - shows also how to decrypt values that are stored encrypted in XML (Inner Stream Encryption) + * though it doesn't decode other values, like dates etc. */ - public void exampleSaxparsing(String resourceName) throws IOException, SAXException, ParserConfigurationException { + public void exampleSaxParsing(String resourceName, Credentials credentials, PrintWriter writer) throws IOException, SAXException, ParserConfigurationException { InputStream encryptedInputStream = getClass().getClassLoader().getResourceAsStream(resourceName); - Credentials credentials = new KdbxCreds("123".getBytes()); KdbxHeader kdbxHeader = new KdbxHeader(); + try (InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, kdbxHeader, encryptedInputStream)) { // use this to decrypt the encrypted fields final StreamEncryptor valueEncryptor = kdbxHeader.getInnerStreamEncryptor(); @@ -44,69 +47,43 @@ public void exampleSaxparsing(String resourceName) throws IOException, SAXExcept SAXParser saxParser = spfactory.newSAXParser(); XMLReader xmlReader = saxParser.getXMLReader(); - xmlReader.setContentHandler(new ContentHandler() { + xmlReader.setContentHandler(new DefaultHandler() { boolean protectedContent = false; @Override - public void setDocumentLocator(Locator locator) { - - } - - @Override - public void startDocument() throws SAXException { - System.out.println("Starting document"); - } - - @Override - public void endDocument() throws SAXException { - System.out.println("Ending document"); - } - - @Override - public void startPrefixMapping(String prefix, String uri) throws SAXException { - - } - - @Override - public void endPrefixMapping(String prefix) throws SAXException { - - } - - @Override - public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { + public void startElement(String uri, String localName, String qName, Attributes atts) { protectedContent = atts.getIndex("Protected") >= 0; - System.out.print("<" + qName + ">"); + writer.append("<") + .append(qName); + for (int i = 0; i < atts.getLength(); i++){ + if (!atts.getLocalName(i).equals("Protected")) { + // we wil decrypt protected values + writer.append(" ") + .append(atts.getLocalName(i)) + .append(" = \"") + .append(atts.getValue(i)) + .append("\""); + } + } + writer.append(">"); } @Override - public void endElement(String uri, String localName, String qName) throws SAXException { - System.out.print(""); + public void endElement(String uri, String localName, String qName) { + writer.append(""); } @Override - public void characters(char[] ch, int start, int length) throws SAXException { + public void characters(char[] ch, int start, int length) { String content = new String(ch, start, length); if (protectedContent) { content = new String(valueEncryptor.decrypt(Helpers.decodeBase64Content(content.getBytes(), false))); } - System.out.print(content); + writer.append(content); protectedContent = false; } - - @Override - public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { - - } - - @Override - public void processingInstruction(String target, String data) throws SAXException { - - } - - @Override - public void skippedEntity(String name) throws SAXException { - - } }); InputSource xmlInputSource = new InputSource(decryptedInputStream); diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/Util.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/Util.java index 26528b4b..2c70a466 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/Util.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/Util.java @@ -1,11 +1,11 @@ package org.linguafranca.pwdb.kdbx; import com.google.common.io.CharStreams; +import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.StreamFormat; +import org.linguafranca.pwdb.kdbx.dom.DomDatabaseWrapper; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.PrintWriter; +import java.io.*; import java.nio.charset.StandardCharsets; public class Util { @@ -25,7 +25,16 @@ public static String streamToString(InputStream inputStream) throws IOException } /** - * Example shows how to list the XML of a database + * Example shows how to list XML with decoded field values (but not decrypted passwords) + */ + public static void listDatabase(String resourceName, Credentials creds, OutputStream outputStream) throws IOException { + DomDatabaseWrapper database = DomDatabaseWrapper.load(creds, Util.class.getClassLoader().getResourceAsStream(resourceName)); + database.save(new StreamFormat.None(), new KdbxCreds.None(), outputStream); + } + + + /** + * Example shows how to list the XML of a database in a raw form * @param resourceName the name of a resource to find on the classpath * @param password the password for the resource * @param printWriter a PrintWriter to list the contents diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/SaxParseTest.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/SaxParseTest.java index f3e7f005..1d673046 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/SaxParseTest.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/SaxParseTest.java @@ -21,19 +21,25 @@ import javax.xml.parsers.ParserConfigurationException; import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; + +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * @author jo */ + public class SaxParseTest extends SaxParse { + OutputStream outputStream = getTestPrintStream(); @Test - public void exampleSaxparsingV3 () throws IOException, SAXException, ParserConfigurationException { - super.exampleSaxparsing("test123.kdbx"); + public void exampleSaxParsingV3 () throws IOException, SAXException, ParserConfigurationException { + super.exampleSaxParsing("test123.kdbx", new KdbxCreds("123".getBytes()), new PrintWriter(outputStream)); } @Test - public void exampleSaxparsingV4 () throws IOException, SAXException, ParserConfigurationException { - super.exampleSaxparsing("V4-AES-Argon2-CustomIcon.kdbx"); + public void exampleSaxParsingV4 () throws IOException, SAXException, ParserConfigurationException { + super.exampleSaxParsing("V4-AES-AES.kdbx", new KdbxCreds("123".getBytes()), new PrintWriter(outputStream)); } } diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java index b77c7251..1d06c598 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java @@ -3,18 +3,32 @@ import org.junit.Test; import java.io.IOException; +import java.io.OutputStream; import java.io.PrintWriter; -import static org.linguafranca.pwdb.kdbx.Util.listKdbxHeaderProperties; -import static org.linguafranca.pwdb.kdbx.Util.listXml; +import static org.linguafranca.pwdb.kdbx.Util.*; +import static org.linguafranca.util.TestUtil.getTestPrintStream; public class UtilTest { + + OutputStream outputStream = getTestPrintStream(); + @Test public void listDatabaseTest() throws IOException { + listDatabase("V4-AES-AES.kdbx", new KdbxCreds("123".getBytes()), outputStream); + } + + @Test + public void listXmlTest() throws IOException { + PrintWriter writer = new PrintWriter(outputStream); + listXml("V4-AES-AES.kdbx", "123".getBytes(), writer); + writer.flush(); + } + /** * List Database Encryption Characteristics */ @Test public void listKdbxHeaderParams () throws IOException { - PrintWriter writer = new PrintWriter(System.out); + PrintWriter writer = new PrintWriter(outputStream); listKdbxHeaderProperties("test123.kdbx", writer); listKdbxHeaderProperties("V4-AES-AES.kdbx", writer); listKdbxHeaderProperties("V4-AES-Argon2.kdbx", writer); @@ -25,7 +39,7 @@ public void listKdbxHeaderParams () throws IOException { @Test public void listHeaderPropertiesAndXml() throws IOException { - PrintWriter writer = new PrintWriter(System.out); + PrintWriter writer = new PrintWriter(outputStream); listKdbxHeaderProperties("V4-AES-Argon2-CustomIcon.kdbx", writer); listXml("V4-AES-Argon2-CustomIcon.kdbx", "123".getBytes(), writer); writer.flush(); diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_27_Test.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_27_Test.java index 56a9ab99..4b898b3a 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_27_Test.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_27_Test.java @@ -13,12 +13,15 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.PrintStream; import java.nio.charset.StandardCharsets; import java.util.List; import static org.junit.Assert.assertEquals; +import static org.linguafranca.util.TestUtil.getTestPrintStream; public class Issue_27_Test { + static PrintStream printStream = getTestPrintStream(); /** * Check load of problem file */ @@ -30,7 +33,7 @@ public void testIssue27() throws IOException { List entries = db.findEntries("testtitle"); for (JaxbEntry entry: entries) { - System.out.println(Helpers.fromDate(entry.getCreationTime())); + printStream.println(Helpers.fromDate(entry.getCreationTime())); assertEquals("2021-01-11T09:18:56Z", Helpers.fromDate(entry.getCreationTime())); } } @@ -46,7 +49,7 @@ public void testV4Date() throws IOException { List entries = db.findEntries("Sample Entry #2 - Copy"); for (JaxbEntry entry: entries) { - System.out.println(Helpers.fromDate(entry.getCreationTime())); + printStream.println(Helpers.fromDate(entry.getCreationTime())); assertEquals("2018-01-26T13:20:58Z", Helpers.fromDate(entry.getCreationTime())); } } @@ -57,6 +60,6 @@ public void testIssue27XML() throws IOException { InputStream is = this.getClass().getClassLoader().getResourceAsStream("issue-27/bogus-timestamp2.kdbx"); KdbxCreds creds = new KdbxCreds("passwordless".getBytes()); InputStream plainText = KdbxSerializer.createUnencryptedInputStream(creds,new KdbxHeader(), is); - System.out.println(CharStreams.toString(new InputStreamReader(plainText, StandardCharsets.UTF_8))); + printStream.println(CharStreams.toString(new InputStreamReader(plainText, StandardCharsets.UTF_8))); } } diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_38_Test.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_38_Test.java index 43066e61..dfda9ffd 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_38_Test.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_38_Test.java @@ -7,13 +7,17 @@ import java.io.IOException; import java.io.InputStream; +import java.io.PrintStream; import java.util.List; +import static org.linguafranca.util.TestUtil.getTestPrintStream; + /** * Bug report on GitHub, the Keyfile is Version 2 (Hex) */ public class Issue_38_Test { + static PrintStream printStream = getTestPrintStream(); @Test public void testV2Keyfile() throws IOException { InputStream databaseStream = Issue_38_Test.class.getClassLoader().getResourceAsStream("issue-38/Database/Database.kdbx"); @@ -24,6 +28,6 @@ public void testV2Keyfile() throws IOException { DomDatabaseWrapper database = DomDatabaseWrapper.load(creds, databaseStream); List entries = database.findEntries("Sample Entry"); DomEntryWrapper entry = entries.get(0); - System.out.println(entry.getTitle()); + printStream.println(entry.getTitle()); } } diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabaseTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabaseTest.java index 1509ed1b..e4d2633f 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabaseTest.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabaseTest.java @@ -21,21 +21,27 @@ import org.linguafranca.pwdb.kdbx.KdbxCreds; import java.io.InputStream; +import java.io.PrintStream; + +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * @author jo */ public class JaxbSerializableDatabaseTest { + + static PrintStream printStream = getTestPrintStream(); + @Test public void createEmptyDatabase() throws Exception { JaxbDatabase db = JaxbDatabase.createEmptyDatabase(); - db.save(new KdbxCreds.None(), System.out); + db.save(new KdbxCreds.None(), printStream); } @Test public void loadXml() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test123.kdbx"); JaxbDatabase database = JaxbDatabase.load(new KdbxCreds("123".getBytes()), inputStream); - database.visit(new Visitor.Print()); + database.visit(new Visitor.Print(printStream)); } } \ No newline at end of file diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbTest.java index 3320ce8b..60aa341d 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbTest.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbTest.java @@ -21,11 +21,15 @@ import org.linguafranca.pwdb.kdbx.jaxb.binding.StringField; import javax.xml.bind.*; +import java.io.PrintStream; + +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * @author jo */ public class JaxbTest { + static PrintStream printStream = getTestPrintStream(); @Test public void unmarshal() throws JAXBException { @@ -34,7 +38,7 @@ public void unmarshal() throws JAXBException { u.setEventHandler(new ValidationEventHandler() { @Override public boolean handleEvent(ValidationEvent event) { - System.out.println(event.getLocator().getLineNumber() +": "+ event.getMessage()); + printStream.println(event.getLocator().getLineNumber() +": "+ event.getMessage()); return true; } }); @@ -43,13 +47,13 @@ public boolean handleEvent(ValidationEvent event) { public void afterUnmarshal(Object target, Object parent) { if (target instanceof StringField.Value) { StringField.Value value = (StringField.Value) target; - System.out.println(value.getValue()); + printStream.println(value.getValue()); } super.afterUnmarshal(target, parent); } }); KeePassFile kpf = (KeePassFile) u.unmarshal(getClass().getClassLoader().getResourceAsStream("ExampleDatabase.xml")); - System.out.println(kpf.getMeta().getDatabaseDescription()); + printStream.println(kpf.getMeta().getDatabaseDescription()); } } diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbV4LoadTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbV4LoadTest.java index 7d076029..66ecd37e 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbV4LoadTest.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbV4LoadTest.java @@ -6,21 +6,27 @@ import org.linguafranca.pwdb.kdbx.KdbxCreds; import java.io.InputStream; +import java.io.PrintStream; + +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * @author jo */ public class JaxbV4LoadTest { + + static PrintStream printStream = getTestPrintStream(); + @Test public void loadKdbxV4() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); JaxbDatabase database = JaxbDatabase.load(new KdbxCreds("123".getBytes()), inputStream); - database.visit(new Visitor.Print()); + database.visit(new Visitor.Print(printStream)); // test what happens to dates in V4 database.visit(new Visitor.Default(){ @Override public void visit(Entry entry) { - System.out.println(entry.getCreationTime()); + printStream.println(entry.getCreationTime()); } }); } diff --git a/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbSerializerTest.java b/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbSerializerTest.java index 25921a2a..06254062 100644 --- a/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbSerializerTest.java +++ b/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbSerializerTest.java @@ -21,16 +21,22 @@ import org.linguafranca.pwdb.Visitor; import java.io.InputStream; +import java.io.PrintStream; + +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * @author jo */ public class KdbSerializerTest { + static PrintStream printStream = getTestPrintStream(); + + @Test public void testCreateKdbDatabase() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test123.kdb"); Database database = KdbDatabase.load(new KdbCredentials.Password("123".getBytes()), inputStream); - database.visit(new Visitor.Print()); + database.visit(new Visitor.Print(printStream)); } } \ No newline at end of file diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/hashedblock/HashBlockTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/hashedblock/HashBlockTest.java index f0557357..14d21611 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/hashedblock/HashBlockTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/hashedblock/HashBlockTest.java @@ -22,11 +22,13 @@ import java.util.Arrays; import static org.junit.Assert.*; +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * @author jo */ public class HashBlockTest { + static PrintStream printStream = getTestPrintStream(); @Test public void testSmallBuf () throws IOException { @@ -108,7 +110,7 @@ private void createFile(File test, int length, byte pattern) throws IOException os.write(buf); os.flush(); os.close(); - System.out.println("File size is " + test.length() + " test buffer is " + length); + printStream.println("File size is " + test.length() + " test buffer is " + length); } } \ No newline at end of file diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelperTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelperTest.java index 3df9dd37..586bcf81 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelperTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelperTest.java @@ -18,33 +18,37 @@ import org.junit.Test; +import java.io.PrintStream; import java.util.UUID; import static org.junit.Assert.assertEquals; +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * tests the operation of Hex conversion */ public class HelperTest { + static PrintStream printStream = getTestPrintStream(); + @Test public void testBase64() throws Exception { String inputString = "0tU8XFRUX2TCk5tmmrshuQ=="; - System.out.println("Input string: " + inputString); + printStream.println("Input string: " + inputString); String hexString = "D2D53C5C54545F64C2939B669ABB21B9"; String convertedString = Helpers.hexStringFromBase64("0tU8XFRUX2TCk5tmmrshuQ=="); - System.out.println("Converted string: " + convertedString); + printStream.println("Converted string: " + convertedString); assertEquals(hexString, convertedString.toUpperCase()); UUID uuid = Helpers.uuidFromBase64(inputString); - System.out.println("UUID: " + uuid.toString()); + printStream.println("UUID: " + uuid.toString()); assertEquals("d2d53c5c-5454-5f64-c293-9b669abb21b9", uuid.toString()); String uuidHex = Helpers.hexStringFromUuid(uuid); - System.out.println("Hex from UUID: " + uuidHex); + printStream.println("Hex from UUID: " + uuidHex); assertEquals(hexString, uuidHex.toUpperCase()); String base64 = Helpers.base64FromUuid(uuid); - System.out.println("Base 64: " + base64); + printStream.println("Base 64: " + base64); assertEquals(inputString, base64); } diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxHeaderTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxHeaderTest.java index 56bf2780..f6a3b440 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxHeaderTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxHeaderTest.java @@ -12,26 +12,29 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.PrintStream; import java.nio.charset.StandardCharsets; import static org.junit.Assert.assertEquals; +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * test decryption of various kinds */ public class KdbxHeaderTest { + static PrintStream printStream = getTestPrintStream(); @Test @Ignore // can be used for detailed HMAC debugging public void getHmacStream() throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); - System.out.println("Version " + header.getVersion()); + printStream.println("Version " + header.getVersion()); KdbxCreds creds = new KdbxCreds("123".getBytes()); assert inputStream != null; //noinspection UnstableApiUsage KdbxSerializer.readOuterHeaderVerification(header, creds, new LittleEndianDataInputStream(inputStream)); HmacBlockInputStream hmacBlockInputStream = new HmacBlockInputStream(header.getHmacKey(creds), inputStream, true); - System.out.println(CharStreams.toString(new InputStreamReader(hmacBlockInputStream, StandardCharsets.UTF_8))); + printStream.println(CharStreams.toString(new InputStreamReader(hmacBlockInputStream, StandardCharsets.UTF_8))); } // check the correct version diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java index 9a76b62b..aa6b4716 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java @@ -23,17 +23,22 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.PrintStream; import java.nio.charset.StandardCharsets; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * tests reading of kdbx with various combinations of key file and password */ public class KdbxKeyFileTest { + + static PrintStream printStream = getTestPrintStream(); + private static void toConsole(InputStream is) throws IOException { - System.out.println(CharStreams.toString(new InputStreamReader(is, StandardCharsets.UTF_8))); + printStream.println(CharStreams.toString(new InputStreamReader(is, StandardCharsets.UTF_8))); } /** * Test that we can load a key file and get a 32 byte base64 encoded value back diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java index fa2ebbcc..27d8011d 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java @@ -25,12 +25,15 @@ import java.util.Scanner; import static org.junit.Assert.assertEquals; +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * test operation of kdbx */ public class KdbxSerializerTest { + static PrintStream printStream = getTestPrintStream(); + /** * Test that we can read a kdbx v3 file and list the XML to console */ @@ -39,7 +42,7 @@ public void testGetPlainTextInputStream() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment.kdbx"); Credentials credentials = new KdbxCreds("123".getBytes()); InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); - System.out.println(CharStreams.toString(new InputStreamReader(decryptedInputStream, StandardCharsets.UTF_8))); + printStream.println(CharStreams.toString(new InputStreamReader(decryptedInputStream, StandardCharsets.UTF_8))); } /** @@ -50,7 +53,7 @@ public void testGetPlainTextInputStream2() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha20-AES.kdbx"); Credentials credentials = new KdbxCreds("123".getBytes()); InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); - System.out.println(CharStreams.toString(new InputStreamReader(decryptedInputStream, StandardCharsets.UTF_8))); + printStream.println(CharStreams.toString(new InputStreamReader(decryptedInputStream, StandardCharsets.UTF_8))); } diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java index 48014e8b..30a77ed6 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java @@ -19,9 +19,11 @@ import org.junit.Test; import org.linguafranca.pwdb.security.StreamEncryptor.*; +import java.io.PrintStream; import java.security.SecureRandom; import static org.junit.Assert.assertEquals; +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * Verify that the inner random encryption works two-way @@ -29,6 +31,8 @@ */ public class StreamEncryptionTest { + static PrintStream printStream = getTestPrintStream(); + @Test public void salsa20() { byte[] key = SecureRandom.getSeed(32); @@ -59,8 +63,8 @@ private void verifyTwoWay(StreamEncryptor ss, StreamEncryptor tt) { assertEquals("new secret", s1); assertEquals("secret 2", t1); - System.out.println(s1); - System.out.println(t1); + printStream.println(s1); + printStream.println(t1); } } \ No newline at end of file diff --git a/logback.xml b/logback.xml new file mode 100644 index 00000000..972c5a5f --- /dev/null +++ b/logback.xml @@ -0,0 +1,21 @@ + + + + + + -%highlight(%-5level) %d{HH:mm:ss.SSS} [%thread] %logger{36} - %cyan(%marker) %msg%n + + + + + + + + + 100 + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 37f64900..ef9f2cd0 100644 --- a/pom.xml +++ b/pom.xml @@ -90,6 +90,22 @@ org.apache.maven.plugins maven-surefire-plugin ${maven-surefire-plugin.version} + + false + + ${inhibitConsoleOutput} + + + + ${maven.multiModuleProjectDirectory}/logback.xml + + + + listener + org.linguafranca.util.MavenRunListener + + + org.apache.maven.plugins @@ -135,6 +151,21 @@ + + + junit + junit + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + @@ -156,17 +187,12 @@ org.slf4j slf4j-api - 1.7.36 - - - ch.qos.logback - logback-core - 1.4.7 + 2.0.5 ch.qos.logback logback-classic - 1.4.7 + 1.3.7 commons-codec @@ -182,6 +208,12 @@ + + inhibitConsoleOutput + + true + + signJar diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java index 94c7380d..fa567fd0 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java @@ -24,12 +24,16 @@ import javax.xml.stream.XMLStreamException; import java.io.IOException; import java.io.InputStream; +import java.io.PrintStream; import java.util.Scanner; +import static org.linguafranca.util.TestUtil.getTestPrintStream; + /** * @author jo */ public class KdbxInnerInputStreamTest { + static PrintStream printStream = getTestPrintStream(); // TODO what does this test, exactly? @Test @@ -38,7 +42,7 @@ public void test() throws XMLStreamException, IOException { XmlInputStreamFilter sxd = new XmlInputStreamFilter(is, new KdbxInputTransformer(new StreamEncryptor.None())); Scanner s = new Scanner(is); while (s.hasNext()) { - System.out.println(s.nextLine()); + printStream.println(s.nextLine()); } s.close(); } diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java index 8c42a892..d91d3395 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java @@ -23,34 +23,39 @@ import org.linguafranca.pwdb.kdbx.KdbxCreds; import java.io.InputStream; +import java.io.PrintStream; + +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * @author jo */ public class SimpleDatabaseLoadTest { + static PrintStream printStream = getTestPrintStream(); + @Test public void loadXml() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("ExampleDatabase.xml"); SimpleDatabase database = SimpleDatabase.loadXml(inputStream); - database.visit(new Visitor.Print()); + database.visit(new Visitor.Print(printStream)); } @Test public void loadKdbx() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test123.kdbx"); SimpleDatabase database = SimpleDatabase.load(new KdbxCreds("123".getBytes()), inputStream); - database.visit(new Visitor.Print()); + database.visit(new Visitor.Print(printStream)); } @Test public void loadKdbxV4() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); SimpleDatabase database = SimpleDatabase.load(new KdbxCreds("123".getBytes()), inputStream); - database.visit(new Visitor.Print()); + database.visit(new Visitor.Print(printStream)); // test what happens to dates in V4 database.visit(new Visitor.Default(){ @Override public void visit(Entry entry) { - System.out.println(entry.getCreationTime()); + printStream.println(entry.getCreationTime()); } }); } @@ -58,14 +63,14 @@ public void visit(Entry entry) { @Test public void emptyDb() throws Exception { SimpleDatabase database = new SimpleDatabase(); - System.out.println(database.getDescription()); + printStream.println(database.getDescription()); } @Test public void dbWithDeleted() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("testDeleted.kdbx"); SimpleDatabase database = SimpleDatabase.load(new KdbxCreds("123".getBytes()), inputStream); - database.visit(new Visitor.Print()); + database.visit(new Visitor.Print(printStream)); } } \ No newline at end of file diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java index a936180b..d848e759 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java @@ -28,11 +28,15 @@ import java.io.*; import static org.junit.Assert.assertTrue; +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * @author jo */ public class SimpleSaveAndReloadTest extends SaveAndReloadChecks { + + static PrintStream printStream = getTestPrintStream(); + @Override public Database getDatabase() { return new SimpleDatabase(); @@ -95,7 +99,7 @@ public void uppercaseBooleanTest() throws IOException { assertTrue(string.contains("True")); foundValue = true; } - System.out.println(string); + printStream.println(string); } assertTrue(foundValue); } diff --git a/test/pom.xml b/test/pom.xml index 154ca957..f695f755 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -17,22 +17,6 @@ database ${project.version} - - junit - junit - - - org.slf4j - slf4j-api - - - ch.qos.logback - logback-core - - - ch.qos.logback - logback-classic - \ No newline at end of file diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java index eb726659..140ea1c1 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java @@ -20,17 +20,22 @@ import org.junit.Test; import org.linguafranca.pwdb.*; +import java.io.PrintStream; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import static org.junit.Assert.assertEquals; +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * @author jo */ public abstract class DatabaseLoaderChecks , G extends Group, E extends Entry, I extends Icon>{ + + static PrintStream printStream = getTestPrintStream(); + protected Database database; protected boolean skipDateCheck = false; private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssX"); @@ -41,7 +46,7 @@ public abstract class DatabaseLoaderChecks , G exten public void test123File() throws ParseException { // visit all groups and entries and list them to console - database.visit(new Visitor.Print()); + database.visit(new Visitor.Print(printStream)); // find all entries in the database // the kdb version has three additional system related entries @@ -51,7 +56,7 @@ public void test123File() throws ParseException { // find all entries in the database that have the string "test" in them List tests = database.findEntries("test"); for (Entry tes: tests) { - System.out.println(tes.getTitle()); + printStream.println(tes.getTitle()); } Assert.assertEquals(4, tests.size()); if (tests.size() > 0) { @@ -60,7 +65,7 @@ public void test123File() throws ParseException { /* StringSelection selection = new StringSelection(pass); Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, selection); - System.out.println(pass + " copied to clip board"); + printStream.println(pass + " copied to clip board"); */ // all the relevant entries should have the password 123 String pass2 = tests.get(0).getPassword(); @@ -72,7 +77,7 @@ public void test123File() throws ParseException { Assert.assertEquals(4, passwords.size()); for (Entry passwordEntry : passwords) { assertEquals(passwordEntry.getTitle(), passwordEntry.getPassword()); - System.out.println(passwordEntry.getTitle()); + printStream.println(passwordEntry.getTitle()); } List entries = database.findEntries(new Entry.Matcher() { diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java index b27a3732..73dce159 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java @@ -28,12 +28,15 @@ import java.util.UUID; import static org.junit.Assert.*; +import static org.linguafranca.util.TestUtil.getTestPrintStream; /** * @author jo */ public abstract class SaveAndReloadChecks , G extends Group, E extends Entry, I extends Icon>{ + static PrintStream printStream = getTestPrintStream(); + public abstract D getDatabase(); public abstract D getDatabase(String name, Credentials credentials) throws IOException; @@ -59,7 +62,7 @@ public void saveAndReloadTest() throws IOException { // create database with known content D output = createNewDatabase(); verifyContents(output); - //output.save(new StreamFormat.None(), new Credentials.None(), System.out); + //output.save(new StreamFormat.None(), new Credentials.None(), printStream); FileOutputStream fos = new FileOutputStream("testOutput/test1.kdbx"); saveDatabase(output, getCreds("123".getBytes()), fos); @@ -68,14 +71,14 @@ public void saveAndReloadTest() throws IOException { fos.close(); // make sure that saving didn't mess up content verifyContents(output); - //output.save(new StreamFormat.None(), new Credentials.None(), System.out); + //output.save(new StreamFormat.None(), new Credentials.None(), printStream); FileInputStream fis = new FileInputStream("testOutput/test1.kdbx"); D input = loadDatabase(getCreds("123".getBytes()), fis); verifyContents(input); - //saveDatabase(input, new StreamFormat.None(), new Credentials.None(), System.out); - System.out.format("Test took %d millis", System.currentTimeMillis() - now); + //saveDatabase(input, new StreamFormat.None(), new Credentials.None(), printStream); + printStream.format("Test took %d millis", System.currentTimeMillis() - now); } /** @@ -106,7 +109,7 @@ public void saveAndReloadTest2() throws IOException { entry = input.findEntries("Test attachment").get(0); assertArrayEquals(new String[] {"letter J.jpeg", "letter L.jpeg"}, entry.getBinaryPropertyNames().toArray()); - //saveDatabase(input, new StreamFormat.None(), new Credentials.None(), System.out); + //saveDatabase(input, new StreamFormat.None(), new Credentials.None(), printStream); } String [] testFiles = {"V4-AES-AES.kdbx", @@ -195,7 +198,7 @@ public void saveNewDatabase () throws IOException { public void inspectNewDatabase () throws IOException { D database = createNewDatabase(); - database.visit(new Visitor.Print()); + database.visit(new Visitor.Print(printStream)); } // create a new database for messing around with diff --git a/test/src/main/java/org/linguafranca/util/HexViewer.java b/test/src/main/java/org/linguafranca/util/HexViewer.java index e68a779e..b562bed6 100644 --- a/test/src/main/java/org/linguafranca/util/HexViewer.java +++ b/test/src/main/java/org/linguafranca/util/HexViewer.java @@ -2,9 +2,17 @@ import java.io.IOException; import java.io.InputStream; +import java.io.PrintStream; +import static org.linguafranca.util.TestUtil.getTestPrintStream; + +/** + * Useful for e.g. viewing the raw file contents + */ public class HexViewer { + static PrintStream printStream = getTestPrintStream(); + public static void main(String[] args) throws IOException { @@ -17,13 +25,13 @@ public static void main(String[] args) throws IOException { is.read(buf); StringBuilder sb = new StringBuilder(); for (byte b: buf) { - sb.append(String.format("%02X", b)); + sb.append(String.format("%02X ", b)); } sb.append(" "); for (byte b : buf) { sb.append(b < 0x20 || b > 0x7e ? (char) 0x00B7 : (char) b); } - System.out.println(sb); + printStream.println(sb); } } } diff --git a/test/src/main/java/org/linguafranca/util/MavenRunListener.java b/test/src/main/java/org/linguafranca/util/MavenRunListener.java new file mode 100644 index 00000000..1839a645 --- /dev/null +++ b/test/src/main/java/org/linguafranca/util/MavenRunListener.java @@ -0,0 +1,55 @@ +package org.linguafranca.util; + +import org.junit.runner.Description; +import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; + +/** + * Helper class to identify the test being run by maven surefire in the log output. This + * is particularly useful if there is a lot of "chatty" testing going on. + *

        + * See: Using Custom Listeners and Reporters + */ +@RunListener.ThreadSafe +public class MavenRunListener extends RunListener { + static Logger logger = LoggerFactory.getLogger("MavenRunListener"); + static Marker marker = MarkerFactory.getMarker("Starting Test"); + static Marker endMarker = MarkerFactory.getMarker("Finished Test"); + static Marker failMarker = MarkerFactory.getMarker("Failed Test"); + static Marker assumeFailMarker = MarkerFactory.getMarker("Assumption Failed"); + static Marker ignoredMarker = MarkerFactory.getMarker("Ignored Test"); + + @Override + public void testStarted(Description description) throws Exception { + super.testStarted(description); + logger.info(marker, "{}", description.getDisplayName()); + } + + @Override + public void testFailure(Failure failure) throws Exception { + super.testFailure(failure); + logger.error(failMarker, "{} {}", failure.getMessage(), failure.getDescription().getDisplayName()); + } + + @Override + public void testAssumptionFailure(Failure failure) { + super.testAssumptionFailure(failure); + logger.warn(assumeFailMarker, "{} {}", failure.getMessage(), failure.getDescription().getDisplayName()); + } + + @Override + public void testIgnored(Description description) throws Exception { + super.testIgnored(description); + logger.info(ignoredMarker, "{}", description.getDisplayName()); + } + + @Override + public void testFinished(Description description) throws Exception { + super.testFinished(description); + logger.info(endMarker, "Finished {}", description.getDisplayName()); + } +} diff --git a/test/src/main/java/org/linguafranca/util/TestUtil.java b/test/src/main/java/org/linguafranca/util/TestUtil.java new file mode 100644 index 00000000..c6433e42 --- /dev/null +++ b/test/src/main/java/org/linguafranca/util/TestUtil.java @@ -0,0 +1,17 @@ +package org.linguafranca.util; + +import com.google.common.io.ByteStreams; + +import java.io.PrintStream; + +public class TestUtil { + /** + * set system property to suppress output from tests + * @return if "nullOutput has been set, e.g. in a profile + */ + public static PrintStream getTestPrintStream() { + return Boolean.getBoolean("inhibitConsoleOutput") ? + new PrintStream(ByteStreams.nullOutputStream()) : + new PrintStream(System.out); + } +} From 4d16cec399a57ed6143e7a7963486add30b085e9 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 12 May 2023 18:58:48 +0100 Subject: [PATCH 097/228] java8 and java 11 circleci config --- config.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 config.yaml diff --git a/config.yaml b/config.yaml new file mode 100644 index 00000000..e69de29b From 60ac75748bdfccdf8f60d8bee57825b3eb4baa22 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 12 May 2023 18:59:13 +0100 Subject: [PATCH 098/228] circle ci config --- config.yaml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/config.yaml b/config.yaml index e69de29b..4a296902 100644 --- a/config.yaml +++ b/config.yaml @@ -0,0 +1,27 @@ +version: 2.1 +jobs: + java8: + docker: + - image: cimg/openjdk:8.0 + working_directory: ~/workspace + steps: + - checkout + - run: + name: Clean and Test JDK 8 + command: mvn -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn clean test + java11: + docker: + - image: cimg/openjdk:11.0 + working_directory: ~/workspace + steps: + - checkout + - run: + name: Clean and Test JDK 11 + command: mvn -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn clean test +workflows: + maven_test: + jobs: + - ubuntu-jdk8 + - ubuntu-jdk11 + - java8 + - java11 From d0545e62599fa094ee302a18d2918191bbe57d29 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 12 May 2023 19:08:35 +0100 Subject: [PATCH 099/228] wrong name for CircleCI YAML --- config.yaml => config.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename config.yaml => config.yml (100%) diff --git a/config.yaml b/config.yml similarity index 100% rename from config.yaml rename to config.yml From 72a135ac5b0f3befca9d38186f172a6dbfc0dd62 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 12 May 2023 19:10:44 +0100 Subject: [PATCH 100/228] wrong directory --- config.yml => .circleci/config.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename config.yml => .circleci/config.yml (100%) diff --git a/config.yml b/.circleci/config.yml similarity index 100% rename from config.yml rename to .circleci/config.yml From 766582868a6c20681fd04beb6024fe272a58423a Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 12 May 2023 20:23:24 +0100 Subject: [PATCH 101/228] Add circle ci conig --- .circleci/config.yml | 58 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..5151a953 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,58 @@ +# Use the latest 2.1 version of CircleCI pipeline process engine. +# See: https://circleci.com/docs/2.0/configuration-reference +version: 2.1 + +# Define a job to be invoked later in a workflow. +# See: https://circleci.com/docs/2.0/configuration-reference/#jobs +jobs: + # Below is the definition of your job to build and test your app, you can rename and customize it as you want. + build-and-test-8: + # These next lines define a Docker executor: https://circleci.com/docs/2.0/executor-types/ + # You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. + # Be sure to update the Docker image tag below to openjdk version of your application. + # A list of available CircleCI Docker Convenience Images are available here: https://circleci.com/developer/images/image/cimg/openjdk + docker: + - image: cimg/openjdk:8.0 + # Add steps to the job + # See: https://circleci.com/docs/2.0/configuration-reference/#steps + steps: + # Checkout the code as the first step. + - checkout + # Use mvn clean and package as the standard maven build phase + - run: + name: Build + command: mvn -B -DskipTests clean package + # Then run your tests! + - run: + name: Test + command: mvn -P inhibitConsoleOutput test + + build-and-test-11: + # These next lines define a Docker executor: https://circleci.com/docs/2.0/executor-types/ + # You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. + # Be sure to update the Docker image tag below to openjdk version of your application. + # A list of available CircleCI Docker Convenience Images are available here: https://circleci.com/developer/images/image/cimg/openjdk + docker: + - image: cimg/openjdk:11.0 + # Add steps to the job + # See: https://circleci.com/docs/2.0/configuration-reference/#steps + steps: + # Checkout the code as the first step. + - checkout + # Use mvn clean and package as the standard maven build phase + - run: + name: Build + command: mvn -B -DskipTests clean package + # Then run your tests! + - run: + name: Test + command: mvn -P inhibitConsoleOutput test + +# Invoke jobs via workflows +# See: https://circleci.com/docs/2.0/configuration-reference/#workflows +workflows: + build-8-11: # This is the name of the workflow, feel free to change it to better match your workflow. + # Inside the workflow, you define the jobs you want to run. + jobs: + - build-and-test-8 + - build-and-test-11 From a45371c9fd2974e054d14290aff51deebfb5aa57 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 13 May 2023 12:03:23 +0100 Subject: [PATCH 102/228] Tidy .gitIgnore --- .gitignore | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index c4a6ad20..e3020a05 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ testOutput -build.gradle build -settings.gradle -gradlew -gradlew.bat -.gradle -gradle *.versionsBackup */target -/KeePass-2.53.1-Source/ +#settings.gradle +#gradlew +#gradlew.bat +#.gradle +#gradle +#build.gradle + From 4616908b2ca546db47a4831fcd334cfe95776392 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 13 May 2023 12:04:37 +0100 Subject: [PATCH 103/228] Add status badges to readme.md --- readme.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index a88f2f89..8c3b6ab5 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,13 @@ # KeePassJava2 -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent) +[![javadoc](https://javadoc.io/badge2/org.linguafranca.pwdb/KeePassJava2/javadoc.svg)](https://javadoc.io/doc/org.linguafranca.pwdb/KeePassJava2) + +![alt text](https://badgen.net/badge/Build/2.2-SNAPSHOT/red?icon=github) +master [![CircleCI](https://dl.circleci.com/status-badge/img/gh/jorabin/KeePassJava2/tree/master.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/jorabin/KeePassJava2/tree/master) +develop [![CircleCI](https://dl.circleci.com/status-badge/img/gh/jorabin/KeePassJava2/tree/develop.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/jorabin/KeePassJava2/tree/develop) + + A Java 8 API for databases compatible with the renowned [KeePass](http://keepass.info) password safe for Windows. This is a "headless" implementation - if you want something with a UI @@ -32,7 +39,7 @@ It is licensed under the Apache 2 License and is currently usable. ## Current Status -After a number of years of neglect, the project is (May 2023) back in development. +After a a period of neglect, the project is (May 2023) back in development. The current code is version 2.2-SNAPSHOT. This is on the main branch. It has not yet been distributed to Maven. See [Build from Source](#build-from-source) From edba9f2217f546840b15da7df00707d867b8bf11 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 13 May 2023 14:35:15 +0100 Subject: [PATCH 104/228] Renqame test classes to adhere to conventions --- .../validation/{Issue_27_Test.java => Issue27Test.java} | 2 +- .../validation/{Issue_38_Test.java => Issue38Test.java} | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename example/src/test/java/org/linguafranca/pwdb/kdbx/validation/{Issue_27_Test.java => Issue27Test.java} (98%) rename example/src/test/java/org/linguafranca/pwdb/kdbx/validation/{Issue_38_Test.java => Issue38Test.java} (78%) diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_27_Test.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue27Test.java similarity index 98% rename from example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_27_Test.java rename to example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue27Test.java index 4b898b3a..6f36177a 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_27_Test.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue27Test.java @@ -20,7 +20,7 @@ import static org.junit.Assert.assertEquals; import static org.linguafranca.util.TestUtil.getTestPrintStream; -public class Issue_27_Test { +public class Issue27Test { static PrintStream printStream = getTestPrintStream(); /** * Check load of problem file diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_38_Test.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue38Test.java similarity index 78% rename from example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_38_Test.java rename to example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue38Test.java index dfda9ffd..4684a712 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue_38_Test.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue38Test.java @@ -15,13 +15,13 @@ /** * Bug report on GitHub, the Keyfile is Version 2 (Hex) */ -public class Issue_38_Test { +public class Issue38Test { static PrintStream printStream = getTestPrintStream(); @Test public void testV2Keyfile() throws IOException { - InputStream databaseStream = Issue_38_Test.class.getClassLoader().getResourceAsStream("issue-38/Database/Database.kdbx"); - InputStream keyStream = Issue_38_Test.class.getClassLoader().getResourceAsStream("issue-38/Database/Database.keyx"); + InputStream databaseStream = Issue38Test.class.getClassLoader().getResourceAsStream("issue-38/Database/Database.kdbx"); + InputStream keyStream = Issue38Test.class.getClassLoader().getResourceAsStream("issue-38/Database/Database.keyx"); assert keyStream != null; KdbxCreds creds = new KdbxCreds("MyPassword".getBytes(), keyStream); assert databaseStream != null; From a84c0654d27e8c1d4d1213d1bf4a42014d3ef98a Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 13 May 2023 14:36:02 +0100 Subject: [PATCH 105/228] Refactor Util to have better signatures --- .../java/org/linguafranca/pwdb/kdbx/Util.java | 15 +++++++-------- .../java/org/linguafranca/pwdb/kdbx/UtilTest.java | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/Util.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/Util.java index 2c70a466..bb5b0d84 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/Util.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/Util.java @@ -10,14 +10,13 @@ public class Util { - public static InputStream getDecryptedInputStream (String resourceName, byte [] password) throws IOException { - return getDecryptedInputStream(resourceName, password, new KdbxHeader()); + public static InputStream getDecryptedInputStream (String resourceName, Credentials credentials) throws IOException { + return getDecryptedInputStream(resourceName, credentials, new KdbxHeader()); } - public static InputStream getDecryptedInputStream (String resourceName, byte [] password, KdbxHeader header) throws IOException { + public static InputStream getDecryptedInputStream (String resourceName, Credentials credentials, KdbxHeader header) throws IOException { InputStream is = Util.class.getClassLoader().getResourceAsStream(resourceName); - KdbxCreds creds = new KdbxCreds(password); - return KdbxSerializer.createUnencryptedInputStream(creds, header, is); + return KdbxSerializer.createUnencryptedInputStream(credentials, header, is); } public static String streamToString(InputStream inputStream) throws IOException { @@ -36,12 +35,12 @@ public static void listDatabase(String resourceName, Credentials creds, OutputSt /** * Example shows how to list the XML of a database in a raw form * @param resourceName the name of a resource to find on the classpath - * @param password the password for the resource + * @param credentials the credentials for the resource * @param printWriter a PrintWriter to list the contents */ - public static void listXml(String resourceName, byte [] password, PrintWriter printWriter) throws IOException { + public static void listXml(String resourceName, Credentials credentials, PrintWriter printWriter) throws IOException { printWriter.format(resourceName + "\n"); - printWriter.println(streamToString(getDecryptedInputStream(resourceName, password))); + printWriter.println(streamToString(getDecryptedInputStream(resourceName, credentials))); printWriter.println(); printWriter.flush(); } diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java index 1d06c598..4770ba7c 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java @@ -19,7 +19,7 @@ public class UtilTest { @Test public void listXmlTest() throws IOException { PrintWriter writer = new PrintWriter(outputStream); - listXml("V4-AES-AES.kdbx", "123".getBytes(), writer); + listXml("V4-AES-AES.kdbx", new KdbxCreds("123".getBytes()), writer); writer.flush(); } @@ -41,7 +41,7 @@ public void listKdbxHeaderParams () throws IOException { public void listHeaderPropertiesAndXml() throws IOException { PrintWriter writer = new PrintWriter(outputStream); listKdbxHeaderProperties("V4-AES-Argon2-CustomIcon.kdbx", writer); - listXml("V4-AES-Argon2-CustomIcon.kdbx", "123".getBytes(), writer); + listXml("V4-AES-Argon2-CustomIcon.kdbx", new KdbxCreds("123".getBytes()), writer); writer.flush(); } } From 5215e9b98ec22fc27182275c5d2ccfff2838dac5 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 13 May 2023 14:36:45 +0100 Subject: [PATCH 106/228] Create a test for Issue #33 --- .../pwdb/kdbx/validation/Issue33Test.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue33Test.java diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue33Test.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue33Test.java new file mode 100644 index 00000000..e9e79cf9 --- /dev/null +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue33Test.java @@ -0,0 +1,60 @@ +package org.linguafranca.pwdb.kdbx.validation; + + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.StreamFormat; +import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.Util; +import org.linguafranca.pwdb.kdbx.dom.DomDatabaseWrapper; +import org.linguafranca.pwdb.kdbx.jaxb.JaxbDatabase; +import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * Review Issue-33 https://github.com/jorabin/KeePassJava2/issues/33 + */ +public class Issue33Test { + + public static final String TEST_RESOURCE = "V4-AES-Argon2-CustomIcon.kdbx"; + public static final String TEST_OUTPUT_DIR = "testOutput"; + public static final File TEST_XML_FILE = Paths.get(TEST_OUTPUT_DIR,"Issue33Source.xml").toFile(); + public static final KdbxCreds CREDENTIALS = new KdbxCreds("123".getBytes()); + + + InputStream inputStream; + + @BeforeClass + public static void listXml() throws IOException { + Files.createDirectories(Paths.get(TEST_OUTPUT_DIR)); + Util.listXml(TEST_RESOURCE, CREDENTIALS, new PrintWriter(TEST_XML_FILE)); + } + + @Before + public void refreshInputStream() { + inputStream = this.getClass().getClassLoader().getResourceAsStream(TEST_RESOURCE); + } + + @Test + public void testDomDatabaseWrapper() throws IOException { + DomDatabaseWrapper database = DomDatabaseWrapper.load(CREDENTIALS, inputStream); + database.save(new StreamFormat.None(), new Credentials.None(), Files.newOutputStream(Paths.get(TEST_OUTPUT_DIR, "Issue33Dom.xml"))); + } + + @Test + public void testJaxbDatabase() throws IOException { + JaxbDatabase database = JaxbDatabase.load(CREDENTIALS, inputStream); + database.save(new StreamFormat.None(), new Credentials.None(), Files.newOutputStream(Paths.get(TEST_OUTPUT_DIR, "Issue33Jaxb.xml"))); + } + + @Test + public void testSimpleDatabase() throws IOException { + SimpleDatabase database = SimpleDatabase.load(CREDENTIALS, inputStream); + database.save(new StreamFormat.None(), new Credentials.None(), Files.newOutputStream(Paths.get(TEST_OUTPUT_DIR, "Issue33Simple.xml"))); + } +} From 6fbebdbf898a460969c3515be6a7fdfa1ec6d677 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 13 May 2023 14:37:36 +0100 Subject: [PATCH 107/228] Fix Simple throws exception on loading custom icon #33 --- .../org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java index b33f582c..08cfb030 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java @@ -215,6 +215,11 @@ public static class Icon implements org.simpleframework.xml.util.Entry { @Element(name = "UUID", type=UUID.class) @Convert(UuidConverter.class) protected UUID uuid; + @Element(name = "Name") + protected String name; + @Element(name = "LastModificationTime", type=Date.class) + @Convert(TimeConverter.class) + protected Date lastModificationTime; @Element(name = "Data") @Convert(Base64ByteArrayConverter.class) protected ByteArray data; From 5298ae0273e93843f0df98e3bb5827b03866857c Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sun, 14 May 2023 20:01:27 +0100 Subject: [PATCH 108/228] DomDatabase serialise values protected on incoming as protected on outgoing, and treat boolean null as null not false --- .../pwdb/kdbx/dom/DomSerializableDatabase.java | 14 +++++++++++--- .../java/org/linguafranca/pwdb/kdbx/Helpers.java | 7 +++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java index 0e313952..fb2d007e 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java @@ -93,6 +93,7 @@ public SerializableDatabase load(InputStream inputStream) throws IOException { String decrypted = new String(encryption.decrypt(encrypted), StandardCharsets.UTF_8); DomHelper.setElementContent(".", element, decrypted); element.removeAttribute("Protected"); + element.setAttribute("kpj2-ProtectOnOutput", "True"); } return this; @@ -126,13 +127,21 @@ public void save(OutputStream outputStream) { // make a copy so we can mess with content Document copyDoc = (Document) doc.cloneNode(true); try { - // check whether protection is required and if so mark the element with @Protected='True' + // check whether protection is required by default and if so mark the element with @Protected='True' prepareProtection(copyDoc, "Title"); prepareProtection(copyDoc, "UserName"); prepareProtection(copyDoc, "Password"); prepareProtection(copyDoc, "Notes"); prepareProtection(copyDoc, "URL"); + // look for elements that were protected on input and mark them as protected again + NodeList shouldProtectedContent = (NodeList) DomHelper.xpath.evaluate("//*[@kpj-ProtectOnOutput='True']", doc, XPathConstants.NODESET); + for (int i = 0; i < shouldProtectedContent.getLength(); i++) { + Element element = (Element) shouldProtectedContent.item(i); + element.removeAttribute("kpj-ProtectOnOutput"); + element.setAttribute("Protected", "True"); + } + // encrypt and base64 every element marked as protected NodeList protectedContent = (NodeList) DomHelper.xpath.evaluate("//*[@Protected='True']", copyDoc, XPathConstants.NODESET); for (int i = 0; i < protectedContent.getLength(); i++){ @@ -152,7 +161,6 @@ public void save(OutputStream outputStream) { } Source xmlSource = new DOMSource(copyDoc); - Result outputTarget = new StreamResult(outputStream); try { TransformerFactory factory = TransformerFactory.newInstance(); //factory.setAttribute("indent-number", "4"); @@ -160,7 +168,7 @@ public void save(OutputStream outputStream) { transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //noinspection HttpUrlsUsage transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); - transformer.transform(xmlSource, outputTarget); + transformer.transform(xmlSource, new StreamResult(outputStream)); } catch (TransformerException e) { throw new IllegalStateException(e); } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java index ff89a587..621ec3e5 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java @@ -72,11 +72,14 @@ public static UUID uuidFromBase64(String base64) { } public static Boolean toBoolean(String value) { - return value.toLowerCase().equals("true") || value.toLowerCase().equals("Yes") || value.equals("1"); + if (value.equalsIgnoreCase("null")) { + return null; + } + return value.equalsIgnoreCase("true") || value.equalsIgnoreCase("yes") || value.equals("1"); } public static String fromBoolean(Boolean value) { - return value == null ? "False" : (value ? "True" : "False"); + return value == null ? "null" : (value ? "True" : "False"); } public static final SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); From df9d76e04513b6bd02c4ee64c9b6ac69fac59f77 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 15 May 2023 15:48:18 +0100 Subject: [PATCH 109/228] Update supporting classes to allow DomDatabaseWrapper to serialise dates V4 wise --- .../java/org/linguafranca/pwdb/Visitor.java | 1 - .../kdbx/dom/DomSerializableDatabase.java | 36 +++++++++----- .../linguafranca/pwdb/kdbx/QuickStart.java | 2 +- .../org/linguafranca/pwdb/kdbx/Helpers.java | 40 ++++++++++++++-- .../pwdb/kdbx/KdbxStreamFormat.java | 1 + .../linguafranca/pwdb/kdbx/HelpersTest.java | 47 +++++++++++++++++++ 6 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelpersTest.java diff --git a/database/src/main/java/org/linguafranca/pwdb/Visitor.java b/database/src/main/java/org/linguafranca/pwdb/Visitor.java index 6cfa57d8..a7e3536b 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Visitor.java +++ b/database/src/main/java/org/linguafranca/pwdb/Visitor.java @@ -91,7 +91,6 @@ public void startVisit(G group) { } @Override - @Deprecated public void visit(E entry) { printStream.println(entry.toString()); } diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java index fb2d007e..b223badc 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java @@ -39,6 +39,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.util.Date; /** * This class is an XML DOM implementation of a KDBX database. The data is maintained as a DOM, @@ -127,25 +128,21 @@ public void save(OutputStream outputStream) { // make a copy so we can mess with content Document copyDoc = (Document) doc.cloneNode(true); try { - // check whether protection is required by default and if so mark the element with @Protected='True' + // check whether protection is required by default and if so mark the element with @@kpj2-ProtectOnOutput='True' prepareProtection(copyDoc, "Title"); prepareProtection(copyDoc, "UserName"); prepareProtection(copyDoc, "Password"); prepareProtection(copyDoc, "Notes"); prepareProtection(copyDoc, "URL"); - // look for elements that were protected on input and mark them as protected again - NodeList shouldProtectedContent = (NodeList) DomHelper.xpath.evaluate("//*[@kpj-ProtectOnOutput='True']", doc, XPathConstants.NODESET); - for (int i = 0; i < shouldProtectedContent.getLength(); i++) { - Element element = (Element) shouldProtectedContent.item(i); - element.removeAttribute("kpj-ProtectOnOutput"); - element.setAttribute("Protected", "True"); - } - // encrypt and base64 every element marked as protected - NodeList protectedContent = (NodeList) DomHelper.xpath.evaluate("//*[@Protected='True']", copyDoc, XPathConstants.NODESET); + NodeList protectedContent = (NodeList) DomHelper.xpath.evaluate("//*[@kpj2-ProtectOnOutput='True']", copyDoc, XPathConstants.NODESET); for (int i = 0; i < protectedContent.getLength(); i++){ Element element = ((Element) protectedContent.item(i)); + + element.removeAttribute("kpj2-ProtectOnOutput"); + element.setAttribute("Protected", "True"); + String decrypted = DomHelper.getElementContent(".", element); if (decrypted == null) { decrypted = ""; @@ -156,6 +153,23 @@ public void save(OutputStream outputStream) { DomHelper.setElementContent(".", element, base64); } + // we need to serialise dates according to the format, and + // we are going to say that anything that is a date is called *Changed or *Time + // because that does work in the schema, but won't work for custom values + NodeList timeBasedContent = (NodeList) DomHelper.xpath.evaluate( + //"//*[ends-with(name(), 'Changed')] | //*[ends-with(name(), 'Time')]", + // dealing with XPath1 + "//*[substring(name(),string-length(name())-6) = 'Changed'] | //*[substring(name(),string-length(name())-3) = 'Time']", + copyDoc, + XPathConstants.NODESET); + for (int i = 0; i < timeBasedContent.getLength(); i++){ + Element element = ((Element) timeBasedContent.item(i)); + String time = DomHelper.getElementContent(".", element); + Date date = Helpers.toDate(time); + String encoded = Helpers.fromDate(date); + DomHelper.setElementContent(".", element, encoded); + } + } catch (XPathExpressionException e) { throw new IllegalStateException(e); } @@ -187,7 +201,7 @@ private void prepareProtection(Document doc, String protect) throws XPathExpress NodeList nodelist = (NodeList) DomHelper.xpath.evaluate(path, doc, XPathConstants.NODESET); for (int i = 0; i < nodelist.getLength(); i++) { Element element = (Element) nodelist.item(i); - element.setAttribute("Protected", "True"); + element.setAttribute("kpj2-ProtectOnOutput", "True"); } } diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java index 9ea05152..e6c7206c 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java @@ -114,7 +114,7 @@ public boolean matches(Entry entry) { G newParent = database.newGroup("Found entries"); // iterate over the found entries for (E entry : entries) { - // copy the entry using DB factory so it remains where it was found as well as being in new group + // copy the entry using DB factory so that it remains where it was found as well as being in new group E copy = database.newEntry(entry); // add new entry to new group newParent.addEntry(copy); diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java index 621ec3e5..f10ec879 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java @@ -25,9 +25,7 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.text.ParseException; import java.text.SimpleDateFormat; -import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; @@ -37,9 +35,15 @@ import java.util.zip.GZIPOutputStream; /** - * @author jo + * The class provides helpers to marshal and unmarshal values of KDBX files */ public class Helpers { + /** + * Oftentimes we have no way of communicating which version we are using, say in an adapter that + * is buried deep in the internals of JAXB marshalling + */ + public static ThreadLocal isV4 = ThreadLocal.withInitial(() -> true); + public static String base64FromUuid(UUID uuid) { byte[] buffer = new byte[16]; ByteBuffer b = ByteBuffer.wrap(buffer); @@ -71,6 +75,9 @@ public static UUID uuidFromBase64(String base64) { return new UUID(b.getLong(), b.getLong(8)); } + /* --- Boolean --- + Booleans are deliberately tri-valued true, false and null + */ public static Boolean toBoolean(String value) { if (value.equalsIgnoreCase("null")) { return null; @@ -82,7 +89,14 @@ public static String fromBoolean(Boolean value) { return value == null ? "null" : (value ? "True" : "False"); } - public static final SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); + /* --- Dates + V3 dates are serialised as ISO8601 using Zulu (Z) TZD - but will deserialize more leniently + V4 dates are base64 encoded seconds since midnight 0001-01-01 + --- */ + + // we use this for formatting a Date which doesn't have a time zone, and we are assuming + // that date is in fact GMT + public static final SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); public static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ"); public static final Date baseDate = Date.from(ZonedDateTime.parse("0001-01-01T00:00:00Z").toInstant()); @@ -102,9 +116,27 @@ public static Date toDate(String value) { return new Date(secondsSinceBaseDate * 1000 + baseDate.getTime()); } + /** + * Formats the value according to the value of {@link Helpers#isV4} + * @param value a date + * @return a formatted date + */ public static String fromDate(Date value) { + return isV4.get() ? fromDateV4(value) : fromDateV3(value); + } + public static String fromDateV3(Date value) { return inFormat.format(value); } + public static String fromDateV4(Date value) { + long keepassInstant = value.getTime() - baseDate.getTime(); + long secondsSinceBaseDate = keepassInstant / 1000; + byte [] asBytes = toBytes(secondsSinceBaseDate, ByteOrder.LITTLE_ENDIAN); + return encodeBase64Content(asBytes); + } + + + + /* --- Base64 --- */ public static byte[] decodeBase64Content(byte[] content) { return decodeBase64Content(content, false); diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java index 2164de49..c6d3c730 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java @@ -78,6 +78,7 @@ public void load(SerializableDatabase serializableDatabase, Credentials credenti @Override public void save(SerializableDatabase serializableDatabase, Credentials credentials, OutputStream outputStream) throws IOException { + Helpers.isV4.set(kdbxHeader.getVersion() == 4); if (kdbxHeader.getVersion() == 4) { // TODO this assumes that the indexes start from 0 and are in sequence ... for (int a = 0; a < serializableDatabase.getBinaryCount(); a++) { diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelpersTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelpersTest.java new file mode 100644 index 00000000..10204d77 --- /dev/null +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelpersTest.java @@ -0,0 +1,47 @@ +package org.linguafranca.pwdb.kdbx; + +import org.junit.Test; + +import java.io.PrintStream; +import java.text.ParseException; +import java.util.Date; + +import static org.junit.Assert.*; +import static org.linguafranca.pwdb.kdbx.Helpers.inFormat; +import static org.linguafranca.util.TestUtil.getTestPrintStream; + +public class HelpersTest { + + static PrintStream printStream = getTestPrintStream(); + + public String testDate = "2023-05-09T17:11:29Z"; + public String v4Encoding = "sWfs2w4AAAA="; + + @Test + public void toDate() throws ParseException { + Date date = Helpers.toDate(v4Encoding); + assertEquals(inFormat.parse(testDate), date); + } + + @Test + public void fromDate() throws ParseException { + Helpers.isV4.set(false); + assertEquals(testDate, Helpers.fromDate(inFormat.parse(testDate))); + Helpers.isV4.set(true); + assertEquals(v4Encoding, Helpers.fromDate(inFormat.parse(testDate))); + } + + @Test + public void fromDateV3() throws ParseException { + Date date = inFormat.parse(testDate); + assertEquals(testDate, Helpers.fromDateV3(date)); + } + + @Test + public void fromDateV4() throws ParseException { + Date date = inFormat.parse(testDate); + String base64 = Helpers.fromDateV4(date); + printStream.println(Helpers.toDate(base64)); + assertEquals(v4Encoding, base64); + } +} \ No newline at end of file From 7b8f075943ecb60c1b3ec2290134ef28495c25a7 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 16 May 2023 00:36:10 +0100 Subject: [PATCH 110/228] More getting dates sorted out --- .../linguafranca/pwdb/kdbx/dom/DomHelper.java | 3 +- .../kdbx/dom/DomSerializableDatabase.java | 8 +++- .../pwdb/kdbx/validation/Issue27Test.java | 6 +-- .../pwdb/kdbx/validation/Issue33Test.java | 2 + .../org/linguafranca/pwdb/kdb/KdbEntry.java | 2 +- .../org/linguafranca/pwdb/kdbx/Helpers.java | 27 +++++++------ .../linguafranca/pwdb/kdbx/HelpersTest.java | 38 +++++++++++++----- .../pwdb/checks/BasicDatabaseChecks.java | 18 +++++---- test/src/main/resources/V3-CustomIcon.kdbx | Bin 0 -> 26046 bytes 9 files changed, 68 insertions(+), 36 deletions(-) create mode 100644 test/src/main/resources/V3-CustomIcon.kdbx diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java index d1cd48af..16fd8594 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java @@ -27,6 +27,7 @@ import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; +import java.time.Instant; import java.util.*; /** @@ -83,7 +84,7 @@ public String getValue() { static class DateValueCreator implements ValueCreator { @Override public String getValue() { - return Helpers.fromDate(new Date()); + return Helpers.fromDate(Date.from(Instant.now())); } } diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java index b223badc..9c5ec683 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java @@ -39,6 +39,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.util.Date; /** @@ -165,7 +166,12 @@ public void save(OutputStream outputStream) { for (int i = 0; i < timeBasedContent.getLength(); i++){ Element element = ((Element) timeBasedContent.item(i)); String time = DomHelper.getElementContent(".", element); - Date date = Helpers.toDate(time); + Date date; + if (time.equals("${creationDate}")) { + date = Date.from(Instant.now()); + } else { + date = Helpers.toDate(time); + } String encoded = Helpers.fromDate(date); DomHelper.setElementContent(".", element, encoded); } diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue27Test.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue27Test.java index 6f36177a..be04ad11 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue27Test.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue27Test.java @@ -33,8 +33,8 @@ public void testIssue27() throws IOException { List entries = db.findEntries("testtitle"); for (JaxbEntry entry: entries) { - printStream.println(Helpers.fromDate(entry.getCreationTime())); - assertEquals("2021-01-11T09:18:56Z", Helpers.fromDate(entry.getCreationTime())); + printStream.println(Helpers.fromDateV3(entry.getCreationTime())); + assertEquals("2021-01-11T09:18:56Z", Helpers.fromDateV3(entry.getCreationTime())); } } @@ -50,7 +50,7 @@ public void testV4Date() throws IOException { for (JaxbEntry entry: entries) { printStream.println(Helpers.fromDate(entry.getCreationTime())); - assertEquals("2018-01-26T13:20:58Z", Helpers.fromDate(entry.getCreationTime())); + assertEquals("2018-01-26T13:20:58Z", Helpers.fromDateV3(entry.getCreationTime())); } } diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue33Test.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue33Test.java index e9e79cf9..09486d82 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue33Test.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue33Test.java @@ -6,6 +6,7 @@ import org.junit.Test; import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.StreamFormat; +import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.kdbx.Util; import org.linguafranca.pwdb.kdbx.dom.DomDatabaseWrapper; @@ -33,6 +34,7 @@ public class Issue33Test { public static void listXml() throws IOException { Files.createDirectories(Paths.get(TEST_OUTPUT_DIR)); Util.listXml(TEST_RESOURCE, CREDENTIALS, new PrintWriter(TEST_XML_FILE)); + Helpers.isV4.set(true); } @Before diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java index 4cd403ad..ac9927b7 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java @@ -41,7 +41,7 @@ public class KdbEntry extends AbstractEntry isV4 = ThreadLocal.withInitial(() -> true); + public static ThreadLocal isV4 = ThreadLocal.withInitial(() -> false); public static String base64FromUuid(UUID uuid) { byte[] buffer = new byte[16]; @@ -96,24 +97,24 @@ V3 dates are serialised as ISO8601 using Zulu (Z) TZD - but will deserialize mor // we use this for formatting a Date which doesn't have a time zone, and we are assuming // that date is in fact GMT - public static final SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - public static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ"); - public static final Date baseDate = Date.from(ZonedDateTime.parse("0001-01-01T00:00:00Z").toInstant()); + //public static final SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + public static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssz"); + public static final Date baseDate = Date.from(ZonedDateTime.parse("0001-01-01T00:00:00Z", dateTimeFormatter).toInstant()); // in V3 this is just a date, in V4 it's a base64 encoded serial number of seconds after the base date above public static Date toDate(String value) { try { - return Date.from(ZonedDateTime.parse(value).toInstant()); + ZonedDateTime zdt = ZonedDateTime.parse(value); + Instant instant = zdt.toInstant(); + return Date.from(instant); } catch (DateTimeParseException e) { - // check if it may be a base64 encoded value ending in = - if (!value.endsWith("=")) { - throw new IllegalArgumentException(value + " is not a valid date"); - } + // let's see if it is a V4 date } // V4 dates are base 64 encoded seconds since baseDate byte [] b = decodeBase64Content(value.getBytes()); long secondsSinceBaseDate = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getLong(); - return new Date(secondsSinceBaseDate * 1000 + baseDate.getTime()); + Instant instant = Instant.ofEpochSecond(secondsSinceBaseDate + baseDate.getTime()/1000); + return Date.from(instant); } /** @@ -125,10 +126,10 @@ public static String fromDate(Date value) { return isV4.get() ? fromDateV4(value) : fromDateV3(value); } public static String fromDateV3(Date value) { - return inFormat.format(value); + return dateTimeFormatter.format(value.toInstant().atZone(ZoneId.of("Z"))); } public static String fromDateV4(Date value) { - long keepassInstant = value.getTime() - baseDate.getTime(); + long keepassInstant = value.toInstant().toEpochMilli() - baseDate.getTime(); long secondsSinceBaseDate = keepassInstant / 1000; byte [] asBytes = toBytes(secondsSinceBaseDate, ByteOrder.LITTLE_ENDIAN); return encodeBase64Content(asBytes); diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelpersTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelpersTest.java index 10204d77..c4ad11aa 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelpersTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelpersTest.java @@ -4,43 +4,63 @@ import java.io.PrintStream; import java.text.ParseException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Date; import static org.junit.Assert.*; -import static org.linguafranca.pwdb.kdbx.Helpers.inFormat; +import static org.linguafranca.pwdb.kdbx.Helpers.dateTimeFormatter; import static org.linguafranca.util.TestUtil.getTestPrintStream; public class HelpersTest { static PrintStream printStream = getTestPrintStream(); - public String testDate = "2023-05-09T17:11:29Z"; + public String testDate = "2023-05-09T16:11:29Z"; + public Date testDateAsDate = Date.from(ZonedDateTime.parse(testDate, dateTimeFormatter).toInstant()); public String v4Encoding = "sWfs2w4AAAA="; @Test public void toDate() throws ParseException { Date date = Helpers.toDate(v4Encoding); - assertEquals(inFormat.parse(testDate), date); + assertEquals(testDateAsDate.getTime(), date.getTime()); + } + + @Test + public void toDate2() throws ParseException { + Date date = Helpers.toDate(testDate); + assertEquals(testDateAsDate, date); + } + + @Test + public void toDate3() throws ParseException { + ZonedDateTime zdt = ZonedDateTime.parse(testDate, dateTimeFormatter); + printStream.println(zdt); + Instant instant = zdt.toInstant(); + printStream.println(instant); + Date date = Date.from(instant); + printStream.println(date.clone()); + assertEquals(instant.toEpochMilli(), date.getTime()); + assertEquals(instant.toEpochMilli(), testDateAsDate.getTime()); } @Test public void fromDate() throws ParseException { Helpers.isV4.set(false); - assertEquals(testDate, Helpers.fromDate(inFormat.parse(testDate))); + assertEquals(testDate, Helpers.fromDate(testDateAsDate)); Helpers.isV4.set(true); - assertEquals(v4Encoding, Helpers.fromDate(inFormat.parse(testDate))); + assertEquals(v4Encoding, Helpers.fromDate(testDateAsDate)); } @Test public void fromDateV3() throws ParseException { - Date date = inFormat.parse(testDate); - assertEquals(testDate, Helpers.fromDateV3(date)); + assertEquals(testDate, Helpers.fromDateV3(testDateAsDate)); } @Test public void fromDateV4() throws ParseException { - Date date = inFormat.parse(testDate); - String base64 = Helpers.fromDateV4(date); + String base64 = Helpers.fromDateV4(testDateAsDate); printStream.println(Helpers.toDate(base64)); assertEquals(v4Encoding, base64); } diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java index cc2c3a38..dd0d06b5 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java @@ -24,6 +24,7 @@ import org.linguafranca.pwdb.Icon; import java.io.IOException; +import java.time.Instant; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -168,20 +169,21 @@ public void testSetFields () { @Test public void testTimes() { - long before = (new Date().getTime() / 1000L) * 1000L; // round to next lower second + long beforeSecond = Instant.now().toEpochMilli()/1000; E entry = database.newEntry(); - long after = (new Date().getTime()/ 1000L) * 1000L; // round to next lower second - long created = entry.getCreationTime().getTime(); - assertTrue(created >= before && created <= after); + long afterSecond = Instant.now().toEpochMilli()/1000; + long createdSecond = entry.getCreationTime().getTime()/1000; + + assertTrue(createdSecond >= beforeSecond && createdSecond <= afterSecond); assertFalse(entry.getExpires()); - assertTrue(entry.getLastAccessTime().getTime() <= created); - assertTrue(entry.getLastModificationTime().getTime() <= created); + assertTrue(entry.getLastAccessTime().getTime()/1000 <= createdSecond); + assertTrue(entry.getLastModificationTime().getTime()/1000 <= createdSecond); entry.setExpires(true); - entry.setExpiryTime(new Date(created)); + entry.setExpiryTime(new Date(createdSecond*1000)); assertTrue(entry.getExpires()); - assertEquals(created, entry.getExpiryTime().getTime()); + assertEquals(createdSecond, entry.getExpiryTime().getTime()/1000); } diff --git a/test/src/main/resources/V3-CustomIcon.kdbx b/test/src/main/resources/V3-CustomIcon.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..e264c6708254960300fc900d0935fb6b4dadde97 GIT binary patch literal 26046 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 JwBx63F|bM@jZFXm literal 0 HcmV?d00001 From e230fd75aabe37abfea1b3d08bd23bf024a25397 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 16 May 2023 00:38:26 +0100 Subject: [PATCH 111/228] Simple protection and date format on output --- .../simple/SimpleSerializableDatabase.java | 10 +++- .../pwdb/kdbx/simple/model/EntryClasses.java | 31 ++++++++---- .../transformer/KdbxInputTransformer.java | 32 +++++++++++-- .../transformer/KdbxOutputTransformer.java | 48 ++++++++++++++----- .../kdbx/simple/SimpleDatabaseLoadTest.java | 3 +- 5 files changed, 98 insertions(+), 26 deletions(-) diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleSerializableDatabase.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleSerializableDatabase.java index 5f669a13..1110dc7a 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleSerializableDatabase.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleSerializableDatabase.java @@ -129,7 +129,15 @@ private static void prepareForSave(SimpleGroup parent){ for (SimpleEntry entry: parent.entry) { for (EntryClasses.StringProperty property : entry.string) { boolean shouldProtect = parent.database.shouldProtect(property.getKey()); - property.getValue().setProtected(shouldProtect); + property.getValue().setProtectOnOutput(shouldProtect || property.getValue().getProtectOnOutput()); + } + if (Objects.nonNull(entry.history)) { + for (SimpleEntry entry2 : entry.history) { + for (EntryClasses.StringProperty property : entry2.string) { + boolean shouldProtect = parent.database.shouldProtect(property.getKey()); + property.getValue().setProtectOnOutput(shouldProtect || property.getValue().getProtectOnOutput()); + } + } } } } diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/EntryClasses.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/EntryClasses.java index b597b3aa..9063437b 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/EntryClasses.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/EntryClasses.java @@ -18,13 +18,12 @@ import org.linguafranca.pwdb.kdbx.simple.SimpleEntry; import org.linguafranca.pwdb.kdbx.simple.converter.KeePassBooleanConverter; -import org.simpleframework.xml.Attribute; -import org.simpleframework.xml.Element; -import org.simpleframework.xml.Root; -import org.simpleframework.xml.Text; +import org.simpleframework.xml.*; import org.simpleframework.xml.convert.Convert; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * @author jo @@ -131,11 +130,18 @@ public Value(String text, Boolean _protected) { @Convert(KeePassBooleanConverter.class) // NB converters don't work on attributes -see KdbxOutputTransformer Boolean _protected; + @Attribute(name = "kpj2-ProtectOnOutput", required = false) + @Convert(KeePassBooleanConverter.class) + Boolean protectOnOutput; @Text String text; - public void setProtected(boolean aProtected) { - this._protected = aProtected; + public void setProtectOnOutput(boolean aProtected) { + this.protectOnOutput = aProtected; + } + + public boolean getProtectOnOutput() { + return Objects.nonNull(this.protectOnOutput) && this.protectOnOutput; } } } @@ -177,9 +183,16 @@ public void setRef(String ref) { } } - @Root(name = "History") public static class History { - @Element(name = "SimpleEntry", required = false) - protected SimpleEntry entry; + + @ElementList(entry = "SimpleEntry", inline = true) + private List list; + + public History(){ + list = new ArrayList<>(); + } + public List getHistory(){ + return list; + } } } diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxInputTransformer.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxInputTransformer.java index 9da9f498..d46982d5 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxInputTransformer.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxInputTransformer.java @@ -20,11 +20,15 @@ import org.linguafranca.pwdb.security.StreamEncryptor; import org.linguafranca.xml.XmlEventTransformer; -import javax.xml.namespace.QName; import javax.xml.stream.XMLEventFactory; import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.StartElement; import javax.xml.stream.events.XMLEvent; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + import static javax.xml.stream.XMLStreamConstants.CHARACTERS; import static javax.xml.stream.XMLStreamConstants.END_ELEMENT; import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; @@ -43,13 +47,35 @@ public class KdbxInputTransformer implements XmlEventTransformer { public KdbxInputTransformer (StreamEncryptor streamEncryptor) { this.streamEncryptor = streamEncryptor; } + XMLEventFactory eventFactory = com.fasterxml.aalto.stax.EventFactoryImpl.newInstance(); public XMLEvent transform (XMLEvent event) { switch (event.getEventType()) { case START_ELEMENT: { - Attribute attribute = event.asStartElement().getAttributeByName(new QName("Protected")); + StartElement startElement = event.asStartElement(); + Iterable attributeIterable = startElement::getAttributes; + List attributes = StreamSupport + .stream(attributeIterable.spliterator(), false) + .collect(Collectors.toList()); + + // find any element that is marked for protection + Attribute attribute = attributes + .stream() + .filter(a -> a.getName().getLocalPart().equalsIgnoreCase("Protected")) + .findFirst() + .orElse(null); + + // set flag so it gets encrypted and remove attribute, set attribute for output if (attribute != null) { - decryptContent = Helpers.toBoolean(attribute.getValue()); + if (attribute.getValue().equalsIgnoreCase("true")) { + decryptContent = true; + attributes.add(eventFactory.createAttribute("kpj2-ProtectOnOutput", "True")); + } + attributes.remove(attribute); + event = eventFactory.createStartElement( + event.asStartElement().getName(), + attributes.iterator(), + null); } break; } diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxOutputTransformer.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxOutputTransformer.java index 28fa3aaa..42d90da8 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxOutputTransformer.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxOutputTransformer.java @@ -23,14 +23,15 @@ import javax.xml.namespace.QName; import javax.xml.stream.XMLEventFactory; import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.StartElement; import javax.xml.stream.events.XMLEvent; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; -import static javax.xml.stream.XMLStreamConstants.CHARACTERS; -import static javax.xml.stream.XMLStreamConstants.END_ELEMENT; -import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; +import static javax.xml.stream.XMLStreamConstants.*; /** * Transform protected elements on output @@ -52,19 +53,36 @@ public KdbxOutputTransformer(StreamEncryptor encryptor) { public XMLEvent transform(XMLEvent event) { switch (event.getEventType()) { case START_ELEMENT: { - Attribute attribute = event.asStartElement().getAttributeByName(new QName("Protected")); + StartElement startElement = event.asStartElement(); + Iterable attributeIterable = startElement::getAttributes; + // filter out the annoying "class" attribute that simple adds to "History" element + // also filter out Protected. + List attributes = StreamSupport + .stream(attributeIterable.spliterator(), false) + .filter(a -> { + String s = a.getName().getLocalPart(); + return(!s.equalsIgnoreCase("class") && !s.equalsIgnoreCase("Protected")); + }) + .collect(Collectors.toList()); + // find any element that is marked for protection + Attribute attribute = attributes + .stream() + .filter(a -> a.getName().getLocalPart().equalsIgnoreCase("kpj2-protectOnOutput")) + .findFirst() + .orElse(null); + // protect it if (attribute != null) { - encryptContent = Helpers.toBoolean(attribute.getValue()); - // this is a workaround for Simple XML not calling converter on attributes - List attributes = new ArrayList<>(); - if (attribute.getValue().toLowerCase().equals("true")) { + if (attribute.getValue().equalsIgnoreCase("true")) { + encryptContent = true; attributes.add(eventFactory.createAttribute("Protected", "True")); } - event = eventFactory.createStartElement( - event.asStartElement().getName(), - attributes.iterator(), - null); + attributes.remove(attribute); } + event = eventFactory.createStartElement( + event.asStartElement().getName(), + attributes.iterator(), + null); + break; } case CHARACTERS: { @@ -72,6 +90,12 @@ public XMLEvent transform(XMLEvent event) { String unencrypted = event.asCharacters().getData(); String encrypted = Helpers.encodeBase64Content(encryptor.encrypt(unencrypted.getBytes()), false); event = eventFactory.createCharacters(encrypted); + } else { + // we want tabs not spaces for indentation + if (event.asCharacters().getData().startsWith("\n")) { + String output = event.asCharacters().getData().replaceAll(" ", "\t"); + event = eventFactory.createCharacters(output); + } } break; } diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java index d91d3395..8feb77c5 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java @@ -16,6 +16,7 @@ package org.linguafranca.pwdb.kdbx.simple; +import org.junit.Ignore; import org.junit.Test; import org.linguafranca.pwdb.Entry; import org.linguafranca.pwdb.Group; @@ -34,7 +35,7 @@ public class SimpleDatabaseLoadTest { static PrintStream printStream = getTestPrintStream(); - @Test + @Test @Ignore(value = "Looks like this example is bogus") public void loadXml() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("ExampleDatabase.xml"); SimpleDatabase database = SimpleDatabase.loadXml(inputStream); From 142af5aadeebbb083057e2d78863073396efcc48 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 16 May 2023 12:25:21 +0100 Subject: [PATCH 112/228] Fix for Simple group to allow CustomIcon #33 --- .../java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java index 4a6235dc..5fb36258 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java @@ -48,6 +48,9 @@ public class SimpleGroup extends org.linguafranca.pwdb.base.AbstractGroup Date: Tue, 16 May 2023 12:26:58 +0100 Subject: [PATCH 113/228] Adding a file chooser to list XML to console --- .../linguafranca/pwdb/kdbx/ChooseFile.java | 72 ++++++++++++++++++ .../java/org/linguafranca/pwdb/kdbx/Util.java | 28 ++++++- .../org/linguafranca/pwdb/kdbx/UtilTest.java | 4 +- test/src/main/resources/V3-CustomIcon.kdbx | Bin 26046 -> 24439 bytes test/src/main/resources/V4-CustomIcon.kdbx | Bin 0 -> 44405 bytes 5 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java create mode 100644 test/src/main/resources/V4-CustomIcon.kdbx diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java new file mode 100644 index 00000000..2f001631 --- /dev/null +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java @@ -0,0 +1,72 @@ +package org.linguafranca.pwdb.kdbx; + +import com.google.common.base.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.*; +import javax.swing.filechooser.FileFilter; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Paths; + +import static org.linguafranca.util.TestUtil.getTestPrintStream; + +public class ChooseFile { + + OutputStream outputStream = getTestPrintStream(); + + public static void main(String[] args) throws URISyntaxException, IOException { + ChooseFile cf = new ChooseFile(); + cf.choose(); + } + + Logger logger = LoggerFactory.getLogger(this.getClass()); + + public ChooseFile(){ + + } + public void choose() throws IOException { + final JFileChooser fc = new JFileChooser(); + URL resources = this.getClass().getClassLoader().getResource("kdb.key"); + try { + fc.setCurrentDirectory(Paths.get(resources.toURI()).toFile()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + fc.setFileFilter(new FileFilter() { + @Override + public boolean accept(File f) { + return f.getName().endsWith(".kdbx"); + } + + @Override + public String getDescription() { + return "KDBX Database"; + } + }); + + int returnVal = fc.showOpenDialog(null); + if (returnVal == JFileChooser.APPROVE_OPTION) { + String s = (String)JOptionPane.showInputDialog( + null, + "Enter the password for " + fc.getSelectedFile().getName(), + "Password", + JOptionPane.PLAIN_MESSAGE, + null, + null, + "123"); + if (Strings.isNullOrEmpty(s)) { + return; + } + logger.info("Opening {}", fc.getSelectedFile().getPath()); + Util.listXml(fc.getSelectedFile().getName(), + new KdbxCreds(s.getBytes()), + new PrintWriter(outputStream)); + } + } +} diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/Util.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/Util.java index bb5b0d84..cfffbe96 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/Util.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/Util.java @@ -2,14 +2,29 @@ import com.google.common.io.CharStreams; import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.kdbx.dom.DomDatabaseWrapper; +import org.linguafranca.pwdb.kdbx.jaxb.JaxbDatabase; +import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; import java.io.*; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; public class Util { + List implementations = new ArrayList<>(Arrays.asList(DomDatabaseWrapper.class, SimpleDatabase.class, JaxbDatabase.class)); + + @FunctionalInterface + public interface DatabaseLoader { + Database load(Credentials c, InputStream i) throws IOException; + } + + List dbLoader = Arrays.asList(DomDatabaseWrapper::load, SimpleDatabase::load, JaxbDatabase::load); + public static InputStream getDecryptedInputStream (String resourceName, Credentials credentials) throws IOException { return getDecryptedInputStream(resourceName, credentials, new KdbxHeader()); } @@ -24,10 +39,18 @@ public static String streamToString(InputStream inputStream) throws IOException } /** - * Example shows how to list XML with decoded field values (but not decrypted passwords) + * Example shows how to list XML from a database (but not decrypted passwords) */ public static void listDatabase(String resourceName, Credentials creds, OutputStream outputStream) throws IOException { - DomDatabaseWrapper database = DomDatabaseWrapper.load(creds, Util.class.getClassLoader().getResourceAsStream(resourceName)); + SimpleDatabase database = SimpleDatabase.load(creds, Util.class.getClassLoader().getResourceAsStream(resourceName)); + database.save(new StreamFormat.None(), new KdbxCreds.None(), outputStream); + } + + /** + * Example shows how to list XML from a database using specified loader + */ + public static void listDatabase(DatabaseLoader loader, String resourceName, Credentials creds, OutputStream outputStream) throws IOException { + Database database = loader.load(creds, Util.class.getClassLoader().getResourceAsStream(resourceName)); database.save(new StreamFormat.None(), new KdbxCreds.None(), outputStream); } @@ -39,7 +62,6 @@ public static void listDatabase(String resourceName, Credentials creds, OutputSt * @param printWriter a PrintWriter to list the contents */ public static void listXml(String resourceName, Credentials credentials, PrintWriter printWriter) throws IOException { - printWriter.format(resourceName + "\n"); printWriter.println(streamToString(getDecryptedInputStream(resourceName, credentials))); printWriter.println(); printWriter.flush(); diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java index 4770ba7c..bfda2c5b 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java @@ -13,13 +13,13 @@ public class UtilTest { OutputStream outputStream = getTestPrintStream(); @Test public void listDatabaseTest() throws IOException { - listDatabase("V4-AES-AES.kdbx", new KdbxCreds("123".getBytes()), outputStream); + listDatabase("V3-CustomIcon.kdbx", new KdbxCreds("123".getBytes()), outputStream); } @Test public void listXmlTest() throws IOException { PrintWriter writer = new PrintWriter(outputStream); - listXml("V4-AES-AES.kdbx", new KdbxCreds("123".getBytes()), writer); + listXml("V3-CustomIcon.kdbx", new KdbxCreds("123".getBytes()), writer); writer.flush(); } diff --git a/test/src/main/resources/V3-CustomIcon.kdbx b/test/src/main/resources/V3-CustomIcon.kdbx index e264c6708254960300fc900d0935fb6b4dadde97..b69439e40ed0edb8b9fee9a33c6488e1d8944414 100644 GIT binary patch literal 24439 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%0DeOOP10WxnLMHfU<%}{vs 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 OHE(V!h_Y$`0000+GEl?- literal 26046 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 JwBx63F|bM@jZFXm diff --git a/test/src/main/resources/V4-CustomIcon.kdbx b/test/src/main/resources/V4-CustomIcon.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..51b1c47efbbd228fcfbf5dd6fe2649a7a91de403 GIT binary patch literal 44405 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 RlqUk-Y6m_iDi8nw001Xu1G4}C literal 0 HcmV?d00001 From 95f58f6a05627cb84efa565296050b0784afe0ff Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 16 May 2023 14:39:00 +0100 Subject: [PATCH 114/228] Move DomHelper so it can be shared with JAXB --- .../src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {dom => kdbx}/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java (100%) diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java similarity index 100% rename from dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java rename to kdbx/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java From 3728a50478264f17c5e1501fdf630641aee99aac Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 16 May 2023 14:41:00 +0100 Subject: [PATCH 115/228] JaxbSerialisable now uses DOM on output --- .../kdbx/jaxb/JaxbSerializableDatabase.java | 94 ++++++++++--------- .../jaxb/JaxbSerializableDatabaseTest.java | 4 +- .../linguafranca/pwdb/kdbx/dom/DomHelper.java | 10 +- 3 files changed, 59 insertions(+), 49 deletions(-) diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java index 5a83fcf2..67d2ebae 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java @@ -17,15 +17,32 @@ package org.linguafranca.pwdb.kdbx.jaxb; import org.apache.commons.codec.binary.Base64; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.linguafranca.pwdb.SerializableDatabase; import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.jaxb.binding.*; +import org.linguafranca.pwdb.kdbx.dom.DomHelper; import org.linguafranca.pwdb.security.StreamEncryptor; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMResult; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -78,7 +95,8 @@ public void afterUnmarshal(Object target, Object parent) { byte[] encrypted = Base64.decodeBase64(value.getValue().getBytes()); String decrypted = new String(encryption.decrypt(encrypted), StandardCharsets.UTF_8); value.setValue(decrypted); - value.setProtected(false); + value.setProtected(null); + value.setProtectInMemory(true); } } if (target instanceof JaxbGroupBinding && (parent instanceof JaxbGroupBinding)) { @@ -115,51 +133,41 @@ public void save(OutputStream outputStream) throws IOException { toEncrypt.add(org.linguafranca.pwdb.Entry.STANDARD_PROPERTY_NAME_NOTES); } try { - JAXBContext jc = JAXBContext.newInstance(KeePassFile.class); - Marshaller u = jc.createMarshaller(); - u.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); - // we encrypt values on marshal and then reset them afterwards - // this may seem a bit clunky, but seems like it's actually less fiddly than other things - u.setListener(new Marshaller.Listener() { - String savedValue = ""; - - /** - * Change protected fields on marshal - * @param source instance of JAXB mapped class prior to marshalling from it. - */ - @Override - public void beforeMarshal(Object source) { - if (source instanceof StringField) { - StringField field = (StringField) source; - if (toEncrypt.contains(field.getKey())) { - this.savedValue = field.getValue().getValue(); - byte[] encrypted = encryption.encrypt(field.getValue().getValue().getBytes()); - String b64 = new String(Base64.encodeBase64(encrypted), StandardCharsets.UTF_8); - field.getValue().setValue(b64); - field.getValue().setProtected(true); - } - } - } + // Create the Document + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbf.newDocumentBuilder(); + Document document = db.newDocument(); - /** - * Restore protected fields after marshal - * @param source instance of JAXB mapped class after marshalling it. - */ - @Override - public void afterMarshal(Object source) { - if (source instanceof StringField) { - StringField field = (StringField) source; - if (Objects.nonNull(field.getValue().getProtected()) && field.getValue().getProtected()) { - field.getValue().setValue(savedValue ); - field.getValue().setProtected(false); - savedValue = ""; - } - } + JAXBContext jc = JAXBContext.newInstance(KeePassFile.class); + Marshaller marshaller = jc.createMarshaller(); + marshaller.marshal(keePassFile, document); + + // encrypt and base64 every element marked as protected + NodeList protectedContent = (NodeList) DomHelper.xpath.evaluate("//*[@Protected='true']", document, XPathConstants.NODESET); + for (int i = 0; i < protectedContent.getLength(); i++){ + Element element = ((Element) protectedContent.item(i)); + String decrypted = DomHelper.getElementContent(".", element); + if (decrypted == null) { + decrypted = ""; } - }); - u.marshal(keePassFile, outputStream); - } catch (JAXBException e) { + byte[] encrypted = encryption.encrypt(decrypted.getBytes()); + // Android compatibility + String base64 = new String(Base64.encodeBase64(encrypted)); + DomHelper.setElementContent(".", element, base64); + } + + try { + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); + transformer.transform(new DOMSource(document), new StreamResult(outputStream)); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } catch (JAXBException | XPathExpressionException e) { throw new IllegalStateException(e); + } catch (ParserConfigurationException e) { + throw new RuntimeException(e); } } diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabaseTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabaseTest.java index e4d2633f..97921e51 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabaseTest.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabaseTest.java @@ -17,6 +17,7 @@ package org.linguafranca.pwdb.kdbx.jaxb; import org.junit.Test; +import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.Visitor; import org.linguafranca.pwdb.kdbx.KdbxCreds; @@ -35,13 +36,14 @@ public class JaxbSerializableDatabaseTest { @Test public void createEmptyDatabase() throws Exception { JaxbDatabase db = JaxbDatabase.createEmptyDatabase(); - db.save(new KdbxCreds.None(), printStream); + db.save(new StreamFormat.None(), new KdbxCreds.None(), printStream); } @Test public void loadXml() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test123.kdbx"); JaxbDatabase database = JaxbDatabase.load(new KdbxCreds("123".getBytes()), inputStream); database.visit(new Visitor.Print(printStream)); + database.save(new StreamFormat.None(), new KdbxCreds.None(), printStream); } } \ No newline at end of file diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java index 16fd8594..7fcb9f70 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java @@ -35,9 +35,9 @@ * * @author jo */ -class DomHelper { +public class DomHelper { - static XPath xpath = XPathFactory.newInstance().newXPath(); + public static XPath xpath = XPathFactory.newInstance().newXPath(); static final String GROUP_ELEMENT_NAME = "Group"; static final String ENTRY_ELEMENT_NAME = "Entry"; @@ -156,13 +156,13 @@ static Element newElement(String elementName, Element parentElement) { } @Nullable - static String getElementContent(String elementPath, Element parentElement) { + public static String getElementContent(String elementPath, Element parentElement) { Element result = getElement(elementPath, parentElement, false); return (result == null) ? null : result.getTextContent(); } @NotNull - static String ensureElementContent(String elementPath, Element parentElement, @NotNull String value) { + public static String ensureElementContent(String elementPath, Element parentElement, @NotNull String value) { Element result = getElement(elementPath, parentElement, false); if (result == null) { result = createHierarchically(elementPath, parentElement); @@ -172,7 +172,7 @@ static String ensureElementContent(String elementPath, Element parentElement, @N } @NotNull - static Element setElementContent(String elementPath, Element parentElement, String value) { + public static Element setElementContent(String elementPath, Element parentElement, String value) { Element result = getElement(elementPath, parentElement, true); result.setTextContent(value); return result; From 086a074685850c5401a1861fa2b059b9cd7f1f60 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 16 May 2023 15:10:59 +0100 Subject: [PATCH 116/228] JAXB save cleanly wrt protection --- .../linguafranca/pwdb/kdbx/ChooseFile.java | 9 +++++--- .../kdbx/jaxb/JaxbSerializableDatabase.java | 23 ++++++++++++------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java index 2f001631..28aa66f4 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java @@ -10,17 +10,19 @@ import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; -import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Paths; import static org.linguafranca.util.TestUtil.getTestPrintStream; +/** + * Utility to allow browsing of database files and listing content to console + */ public class ChooseFile { OutputStream outputStream = getTestPrintStream(); - public static void main(String[] args) throws URISyntaxException, IOException { + public static void main(String[] args) throws IOException { ChooseFile cf = new ChooseFile(); cf.choose(); } @@ -32,10 +34,11 @@ public ChooseFile(){ } public void choose() throws IOException { final JFileChooser fc = new JFileChooser(); + // get a file in the test resource directory URL resources = this.getClass().getClassLoader().getResource("kdb.key"); try { fc.setCurrentDirectory(Paths.get(resources.toURI()).toFile()); - } catch (URISyntaxException e) { + } catch (Exception e) { throw new RuntimeException(e); } fc.setFileFilter(new FileFilter() { diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java index 67d2ebae..81b7697d 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java @@ -143,17 +143,24 @@ public void save(OutputStream outputStream) throws IOException { marshaller.marshal(keePassFile, document); // encrypt and base64 every element marked as protected - NodeList protectedContent = (NodeList) DomHelper.xpath.evaluate("//*[@Protected='true']", document, XPathConstants.NODESET); + NodeList protectedContent = (NodeList) DomHelper.xpath.evaluate("//Value", document, XPathConstants.NODESET); for (int i = 0; i < protectedContent.getLength(); i++){ Element element = ((Element) protectedContent.item(i)); - String decrypted = DomHelper.getElementContent(".", element); - if (decrypted == null) { - decrypted = ""; + boolean protect = element.getAttribute("ProtectInMemory").equalsIgnoreCase("true"); + if (protect) { + String decrypted = DomHelper.getElementContent(".", element); + if (decrypted == null) { + decrypted = ""; + } + byte[] encrypted = encryption.encrypt(decrypted.getBytes()); + // Android compatibility + String base64 = new String(Base64.encodeBase64(encrypted)); + DomHelper.setElementContent(".", element, base64); + element.setAttribute("Protected", "True"); + } else { + element.removeAttribute("Protected"); } - byte[] encrypted = encryption.encrypt(decrypted.getBytes()); - // Android compatibility - String base64 = new String(Base64.encodeBase64(encrypted)); - DomHelper.setElementContent(".", element, base64); + element.removeAttribute("ProtectInMemory"); } try { From a0b754567ad9b60597114b4e1dbe383e0807a39c Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 16 May 2023 19:18:31 +0100 Subject: [PATCH 117/228] first pass adding a parent to String/Value - doesn't deserialise value correctly --- KDBX.4.xsd | 13 +- jaxb/bindings.xml | 11 +- jaxb/pom.xml | 5 +- .../generated/java/META-INF/sun-jaxb.episode | 20 +- .../pwdb/kdbx/jaxb/binding/Adapter1.java | 4 +- .../pwdb/kdbx/jaxb/binding/Adapter2.java | 4 +- .../pwdb/kdbx/jaxb/binding/Adapter3.java | 4 +- .../pwdb/kdbx/jaxb/binding/AutoType.java | 84 ++--- .../pwdb/kdbx/jaxb/binding/Binaries.java | 56 ++-- .../pwdb/kdbx/jaxb/binding/BinaryField.java | 54 ++-- .../pwdb/kdbx/jaxb/binding/CustomData.java | 22 +- .../pwdb/kdbx/jaxb/binding/CustomIcons.java | 64 ++-- .../pwdb/kdbx/jaxb/binding/History.java | 22 +- .../kdbx/jaxb/binding/JaxbEntryBinding.java | 80 +++-- .../kdbx/jaxb/binding/JaxbGroupBinding.java | 108 +++++-- .../pwdb/kdbx/jaxb/binding/KeePassFile.java | 289 +++++++++--------- .../pwdb/kdbx/jaxb/binding/ObjectFactory.java | 56 ++-- .../pwdb/kdbx/jaxb/binding/Parented.java | 8 +- .../pwdb/kdbx/jaxb/binding/StringField.java | 110 +++---- .../pwdb/kdbx/jaxb/binding/Times.java | 34 +-- .../pwdb/kdbx/jaxb/binding/ValueExtender.java | 70 +++++ .../kdbx/jaxb/JaxbSerializableDatabase.java | 3 +- .../pwdb/kdbx/jaxb/base/ValueBinding.java | 24 ++ 23 files changed, 661 insertions(+), 484 deletions(-) create mode 100644 jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ValueExtender.java create mode 100644 jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/base/ValueBinding.java diff --git a/KDBX.4.xsd b/KDBX.4.xsd index bf25f2df..be556bcf 100644 --- a/KDBX.4.xsd +++ b/KDBX.4.xsd @@ -163,6 +163,7 @@ + @@ -319,6 +320,12 @@ + + + + + + This is where the values of the database are actually stored. You can have String valued @@ -332,9 +339,9 @@ - - - + + + diff --git a/jaxb/bindings.xml b/jaxb/bindings.xml index fdec4d8b..75693d85 100644 --- a/jaxb/bindings.xml +++ b/jaxb/bindings.xml @@ -19,7 +19,7 @@ - + + + + + + + - + This class, while generated, is not used see {@link org.linguafranca.pwdb.kdbx.jaxb.org.linguafranca.pwdb.base.AbstractJaxbParentedBinding} which is the actual implementation class. + diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 11be743d..95515f38 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -75,6 +75,7 @@ generateSources + @@ -90,9 +91,9 @@ - + - ../KDBX.3.1.xsd + ../KDBX.4.xsd bindings.xml diff --git a/jaxb/src/generated/java/META-INF/sun-jaxb.episode b/jaxb/src/generated/java/META-INF/sun-jaxb.episode index 623b3196..c585a686 100644 --- a/jaxb/src/generated/java/META-INF/sun-jaxb.episode +++ b/jaxb/src/generated/java/META-INF/sun-jaxb.episode @@ -5,7 +5,7 @@ This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 See http://java.sun.com/xml/jaxb Any modifications to this file will be lost upon recompilation of the source schema. -Generated on: 2016.10.14 at 01:48:37 PM BST +Generated on: 2023.05.16 at 03:17:45 PM BST --> @@ -18,12 +18,12 @@ Generated on: 2016.10.14 at 01:48:37 PM BST - - - + + + @@ -31,20 +31,20 @@ Generated on: 2016.10.14 at 01:48:37 PM BST - + - - + + + + + - - - diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter1.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter1.java index 5e5da1eb..dd791bf6 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter1.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter1.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.16 at 04:44:10 PM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter2.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter2.java index 2407ecb2..947683fb 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter2.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter2.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.16 at 04:44:10 PM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter3.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter3.java index 3510a4dc..c2b9ef09 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter3.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter3.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.16 at 04:44:10 PM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/AutoType.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/AutoType.java index 03737571..ad14484b 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/AutoType.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/AutoType.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.16 at 04:44:10 PM BST // @@ -30,36 +30,36 @@ *

        The following schema fragment specifies the expected content contained within this class. * *

        - * <complexType name="autoType">
        - *   <complexContent>
        - *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        - *       <sequence>
        - *         <element name="Enabled" type="{}keepassBoolean"/>
        - *         <element name="DataTransferObfuscation">
        - *           <simpleType>
        - *             <restriction base="{http://www.w3.org/2001/XMLSchema}int">
        - *               <enumeration value="0"/>
        - *               <enumeration value="1"/>
        - *             </restriction>
        - *           </simpleType>
        - *         </element>
        - *         <element name="DefaultSequence" type="{http://www.w3.org/2001/XMLSchema}string"/>
        - *         <element name="Association">
        - *           <complexType>
        - *             <complexContent>
        - *               <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        - *                 <sequence maxOccurs="unbounded" minOccurs="0">
        - *                   <element name="Window" type="{http://www.w3.org/2001/XMLSchema}string"/>
        - *                   <element name="KeystrokeSequence" type="{http://www.w3.org/2001/XMLSchema}string"/>
        - *                 </sequence>
        - *               </restriction>
        - *             </complexContent>
        - *           </complexType>
        - *         </element>
        - *       </sequence>
        - *     </restriction>
        - *   </complexContent>
        - * </complexType>
        + * <complexType name="autoType">
        + *   <complexContent>
        + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        + *       <sequence>
        + *         <element name="Enabled" type="{}keepassBoolean"/>
        + *         <element name="DataTransferObfuscation">
        + *           <simpleType>
        + *             <restriction base="{http://www.w3.org/2001/XMLSchema}int">
        + *               <enumeration value="0"/>
        + *               <enumeration value="1"/>
        + *             </restriction>
        + *           </simpleType>
        + *         </element>
        + *         <element name="DefaultSequence" type="{http://www.w3.org/2001/XMLSchema}string"/>
        + *         <element name="Association">
        + *           <complexType>
        + *             <complexContent>
        + *               <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        + *                 <sequence maxOccurs="unbounded" minOccurs="0">
        + *                   <element name="Window" type="{http://www.w3.org/2001/XMLSchema}string"/>
        + *                   <element name="KeystrokeSequence" type="{http://www.w3.org/2001/XMLSchema}string"/>
        + *                 </sequence>
        + *               </restriction>
        + *             </complexContent>
        + *           </complexType>
        + *         </element>
        + *       </sequence>
        + *     </restriction>
        + *   </complexContent>
        + * </complexType>
          * 
        * * @@ -178,16 +178,16 @@ public void setAssociation(AutoType.Association value) { *

        The following schema fragment specifies the expected content contained within this class. * *

        -     * <complexType>
        -     *   <complexContent>
        -     *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        -     *       <sequence maxOccurs="unbounded" minOccurs="0">
        -     *         <element name="Window" type="{http://www.w3.org/2001/XMLSchema}string"/>
        -     *         <element name="KeystrokeSequence" type="{http://www.w3.org/2001/XMLSchema}string"/>
        -     *       </sequence>
        -     *     </restriction>
        -     *   </complexContent>
        -     * </complexType>
        +     * <complexType>
        +     *   <complexContent>
        +     *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        +     *       <sequence maxOccurs="unbounded" minOccurs="0">
        +     *         <element name="Window" type="{http://www.w3.org/2001/XMLSchema}string"/>
        +     *         <element name="KeystrokeSequence" type="{http://www.w3.org/2001/XMLSchema}string"/>
        +     *       </sequence>
        +     *     </restriction>
        +     *   </complexContent>
        +     * </complexType>
              * 
        * * diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Binaries.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Binaries.java index b74eb207..6ec3ce9d 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Binaries.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Binaries.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.16 at 04:44:10 PM BST // @@ -29,24 +29,24 @@ *

        The following schema fragment specifies the expected content contained within this class. * *

        - * <complexType name="binaries">
        - *   <complexContent>
        - *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        - *       <sequence maxOccurs="unbounded" minOccurs="0">
        - *         <element name="Binary">
        - *           <complexType>
        - *             <simpleContent>
        - *               <extension base="<http://www.w3.org/2001/XMLSchema>base64Binary">
        - *                 <attribute name="ID" type="{http://www.w3.org/2001/XMLSchema}int" />
        - *                 <attribute name="Compressed" type="{}keepassBoolean" default="False" />
        - *               </extension>
        - *             </simpleContent>
        - *           </complexType>
        - *         </element>
        - *       </sequence>
        - *     </restriction>
        - *   </complexContent>
        - * </complexType>
        + * <complexType name="binaries">
        + *   <complexContent>
        + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        + *       <sequence maxOccurs="unbounded" minOccurs="0">
        + *         <element name="Binary">
        + *           <complexType>
        + *             <simpleContent>
        + *               <extension base="<http://www.w3.org/2001/XMLSchema>base64Binary">
        + *                 <attribute name="ID" type="{http://www.w3.org/2001/XMLSchema}int" />
        + *                 <attribute name="Compressed" type="{}keepassBoolean" default="False" />
        + *               </extension>
        + *             </simpleContent>
        + *           </complexType>
        + *         </element>
        + *       </sequence>
        + *     </restriction>
        + *   </complexContent>
        + * </complexType>
          * 
        * * @@ -96,14 +96,14 @@ public List getBinary() { *

        The following schema fragment specifies the expected content contained within this class. * *

        -     * <complexType>
        -     *   <simpleContent>
        -     *     <extension base="<http://www.w3.org/2001/XMLSchema>base64Binary">
        -     *       <attribute name="ID" type="{http://www.w3.org/2001/XMLSchema}int" />
        -     *       <attribute name="Compressed" type="{}keepassBoolean" default="False" />
        -     *     </extension>
        -     *   </simpleContent>
        -     * </complexType>
        +     * <complexType>
        +     *   <simpleContent>
        +     *     <extension base="<http://www.w3.org/2001/XMLSchema>base64Binary">
        +     *       <attribute name="ID" type="{http://www.w3.org/2001/XMLSchema}int" />
        +     *       <attribute name="Compressed" type="{}keepassBoolean" default="False" />
        +     *     </extension>
        +     *   </simpleContent>
        +     * </complexType>
              * 
        * * diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/BinaryField.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/BinaryField.java index 3b97d416..045583c4 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/BinaryField.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/BinaryField.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.16 at 04:44:10 PM BST // @@ -28,24 +28,24 @@ *

        The following schema fragment specifies the expected content contained within this class. * *

        - * <complexType name="binaryField">
        - *   <complexContent>
        - *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        - *       <sequence>
        - *         <element name="Key" type="{http://www.w3.org/2001/XMLSchema}string"/>
        - *         <element name="Value">
        - *           <complexType>
        - *             <complexContent>
        - *               <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        - *                 <attribute name="Ref" type="{http://www.w3.org/2001/XMLSchema}int" />
        - *               </restriction>
        - *             </complexContent>
        - *           </complexType>
        - *         </element>
        - *       </sequence>
        - *     </restriction>
        - *   </complexContent>
        - * </complexType>
        + * <complexType name="binaryField">
        + *   <complexContent>
        + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        + *       <sequence>
        + *         <element name="Key" type="{http://www.w3.org/2001/XMLSchema}string"/>
        + *         <element name="Value">
        + *           <complexType>
        + *             <complexContent>
        + *               <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        + *                 <attribute name="Ref" type="{http://www.w3.org/2001/XMLSchema}int" />
        + *               </restriction>
        + *             </complexContent>
        + *           </complexType>
        + *         </element>
        + *       </sequence>
        + *     </restriction>
        + *   </complexContent>
        + * </complexType>
          * 
        * * @@ -117,13 +117,13 @@ public void setValue(BinaryField.Value value) { *

        The following schema fragment specifies the expected content contained within this class. * *

        -     * <complexType>
        -     *   <complexContent>
        -     *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        -     *       <attribute name="Ref" type="{http://www.w3.org/2001/XMLSchema}int" />
        -     *     </restriction>
        -     *   </complexContent>
        -     * </complexType>
        +     * <complexType>
        +     *   <complexContent>
        +     *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        +     *       <attribute name="Ref" type="{http://www.w3.org/2001/XMLSchema}int" />
        +     *     </restriction>
        +     *   </complexContent>
        +     * </complexType>
              * 
        * * diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomData.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomData.java index 45acdbe4..43f2c041 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomData.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomData.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.16 at 04:44:10 PM BST // @@ -26,15 +26,15 @@ *

        The following schema fragment specifies the expected content contained within this class. * *

        - * <complexType name="customData">
        - *   <complexContent>
        - *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        - *       <sequence>
        - *         <any maxOccurs="unbounded" minOccurs="0"/>
        - *       </sequence>
        - *     </restriction>
        - *   </complexContent>
        - * </complexType>
        + * <complexType name="customData">
        + *   <complexContent>
        + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        + *       <sequence>
        + *         <any maxOccurs="unbounded" minOccurs="0"/>
        + *       </sequence>
        + *     </restriction>
        + *   </complexContent>
        + * </complexType>
          * 
        * * diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomIcons.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomIcons.java index 057ed766..eff8ee36 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomIcons.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomIcons.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.16 at 04:44:10 PM BST // @@ -25,26 +25,26 @@ *

        The following schema fragment specifies the expected content contained within this class. * *

        - * <complexType name="customIcons">
        - *   <complexContent>
        - *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        - *       <sequence maxOccurs="unbounded">
        - *         <element name="Icon">
        - *           <complexType>
        - *             <complexContent>
        - *               <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        - *                 <sequence>
        - *                   <element name="UUID" type="{}uuid"/>
        - *                   <element name="Data" type="{http://www.w3.org/2001/XMLSchema}base64Binary"/>
        - *                 </sequence>
        - *               </restriction>
        - *             </complexContent>
        - *           </complexType>
        - *         </element>
        - *       </sequence>
        - *     </restriction>
        - *   </complexContent>
        - * </complexType>
        + * <complexType name="customIcons">
        + *   <complexContent>
        + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        + *       <sequence maxOccurs="unbounded">
        + *         <element name="Icon">
        + *           <complexType>
        + *             <complexContent>
        + *               <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        + *                 <sequence>
        + *                   <element name="UUID" type="{}uuid"/>
        + *                   <element name="Data" type="{http://www.w3.org/2001/XMLSchema}base64Binary"/>
        + *                 </sequence>
        + *               </restriction>
        + *             </complexContent>
        + *           </complexType>
        + *         </element>
        + *       </sequence>
        + *     </restriction>
        + *   </complexContent>
        + * </complexType>
          * 
        * * @@ -94,16 +94,16 @@ public List getIcon() { *

        The following schema fragment specifies the expected content contained within this class. * *

        -     * <complexType>
        -     *   <complexContent>
        -     *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        -     *       <sequence>
        -     *         <element name="UUID" type="{}uuid"/>
        -     *         <element name="Data" type="{http://www.w3.org/2001/XMLSchema}base64Binary"/>
        -     *       </sequence>
        -     *     </restriction>
        -     *   </complexContent>
        -     * </complexType>
        +     * <complexType>
        +     *   <complexContent>
        +     *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        +     *       <sequence>
        +     *         <element name="UUID" type="{}uuid"/>
        +     *         <element name="Data" type="{http://www.w3.org/2001/XMLSchema}base64Binary"/>
        +     *       </sequence>
        +     *     </restriction>
        +     *   </complexContent>
        +     * </complexType>
              * 
        * * diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/History.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/History.java index 2c709c88..2db87395 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/History.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/History.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.16 at 04:44:10 PM BST // @@ -21,15 +21,15 @@ *

        The following schema fragment specifies the expected content contained within this class. * *

        - * <complexType>
        - *   <complexContent>
        - *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        - *       <sequence>
        - *         <element ref="{}Entry"/>
        - *       </sequence>
        - *     </restriction>
        - *   </complexContent>
        - * </complexType>
        + * <complexType>
        + *   <complexContent>
        + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        + *       <sequence>
        + *         <element ref="{}Entry"/>
        + *       </sequence>
        + *     </restriction>
        + *   </complexContent>
        + * </complexType>
          * 
        * * diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/JaxbEntryBinding.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/JaxbEntryBinding.java index ae9bdf0e..22edbbbc 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/JaxbEntryBinding.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/JaxbEntryBinding.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.16 at 04:44:10 PM BST // @@ -27,28 +27,29 @@ *

        The following schema fragment specifies the expected content contained within this class. * *

        - * <element name="Entry">
        - *   <complexType>
        - *     <complexContent>
        - *       <extension org.linguafranca.pwdb.base="{}Parented">
        - *         <sequence>
        - *           <element name="UUID" type="{}uuid"/>
        - *           <element name="IconID" type="{}iconId"/>
        - *           <element name="CustomIconUUID" type="{}customIconUuidRef" minOccurs="0"/>
        - *           <element name="ForegroundColor" type="{}color"/>
        - *           <element name="BackgroundColor" type="{}color"/>
        - *           <element name="OverrideURL" type="{}overrideUrl"/>
        - *           <element name="Tags" type="{}commaSeparatedList"/>
        - *           <element ref="{}Times"/>
        - *           <element name="String" type="{}stringField" maxOccurs="unbounded" minOccurs="0"/>
        - *           <element name="Binary" type="{}binaryField" maxOccurs="unbounded" minOccurs="0"/>
        - *           <element name="AutoType" type="{}autoType"/>
        - *           <element ref="{}History" minOccurs="0"/>
        - *         </sequence>
        - *       </extension>
        - *     </complexContent>
        - *   </complexType>
        - * </element>
        + * <element name="Entry">
        + *   <complexType>
        + *     <complexContent>
        + *       <extension base="{}Parented">
        + *         <sequence>
        + *           <element name="UUID" type="{}uuid"/>
        + *           <element name="IconID" type="{}iconId"/>
        + *           <element name="CustomIconUUID" type="{}customIconUuidRef" minOccurs="0"/>
        + *           <element name="ForegroundColor" type="{}color"/>
        + *           <element name="BackgroundColor" type="{}color"/>
        + *           <element name="OverrideURL" type="{}overrideUrl"/>
        + *           <element name="Tags" type="{}commaSeparatedList"/>
        + *           <element ref="{}Times"/>
        + *           <element name="String" type="{}stringField" maxOccurs="unbounded" minOccurs="0"/>
        + *           <element name="Binary" type="{}binaryField" maxOccurs="unbounded" minOccurs="0"/>
        + *           <element name="AutoType" type="{}autoType"/>
        + *           <element ref="{}History" minOccurs="0"/>
        + *           <element name="CustomData" type="{}customData" minOccurs="0"/>
        + *         </sequence>
        + *       </extension>
        + *     </complexContent>
        + *   </complexType>
        + * </element>
          * 
        * * @@ -66,7 +67,8 @@ "string", "binary", "autoType", - "history" + "history", + "customData" }) @XmlRootElement(name = "Entry") public class JaxbEntryBinding @@ -84,10 +86,8 @@ public class JaxbEntryBinding @XmlSchemaType(name = "base64Binary") protected UUID customIconUUID; @XmlElement(name = "ForegroundColor", required = true) - @XmlSchemaType(name = "anySimpleType") protected String foregroundColor; @XmlElement(name = "BackgroundColor", required = true) - @XmlSchemaType(name = "anySimpleType") protected String backgroundColor; @XmlElement(name = "OverrideURL", required = true) protected String overrideURL; @@ -103,6 +103,8 @@ public class JaxbEntryBinding protected AutoType autoType; @XmlElement(name = "History") protected History history; + @XmlElement(name = "CustomData") + protected CustomData customData; /** * Gets the value of the uuid property. @@ -394,4 +396,28 @@ public void setHistory(History value) { this.history = value; } + /** + * Gets the value of the customData property. + * + * @return + * possible object is + * {@link CustomData } + * + */ + public CustomData getCustomData() { + return customData; + } + + /** + * Sets the value of the customData property. + * + * @param value + * allowed object is + * {@link CustomData } + * + */ + public void setCustomData(CustomData value) { + this.customData = value; + } + } diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/JaxbGroupBinding.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/JaxbGroupBinding.java index 98731831..dfedbbf2 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/JaxbGroupBinding.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/JaxbGroupBinding.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.16 at 04:44:10 PM BST // @@ -27,28 +27,30 @@ *

        The following schema fragment specifies the expected content contained within this class. * *

        - * <element name="Group">
        - *   <complexType>
        - *     <complexContent>
        - *       <extension base="{}Parented">
        - *         <sequence>
        - *           <element name="UUID" type="{}uuid"/>
        - *           <element name="Name" type="{http://www.w3.org/2001/XMLSchema}string"/>
        - *           <element name="Notes" type="{http://www.w3.org/2001/XMLSchema}string"/>
        - *           <element name="IconID" type="{}iconId"/>
        - *           <element ref="{}Times"/>
        - *           <element name="IsExpanded" type="{}keepassBoolean"/>
        - *           <element name="DefaultAutoTypeSequence" type="{http://www.w3.org/2001/XMLSchema}string"/>
        - *           <element name="EnableAutoType" type="{}keepassBoolean"/>
        - *           <element name="EnableSearching" type="{}keepassBoolean"/>
        - *           <element name="LastTopVisibleEntry" type="{}uuidRef"/>
        - *           <element ref="{}Entry" maxOccurs="unbounded" minOccurs="0"/>
        - *           <element ref="{}Group" maxOccurs="unbounded" minOccurs="0"/>
        - *         </sequence>
        - *       </extension>
        - *     </complexContent>
        - *   </complexType>
        - * </element>
        + * <element name="Group">
        + *   <complexType>
        + *     <complexContent>
        + *       <extension base="{}Parented">
        + *         <sequence>
        + *           <element name="UUID" type="{}uuid"/>
        + *           <element name="Name" type="{http://www.w3.org/2001/XMLSchema}string"/>
        + *           <element name="Notes" type="{http://www.w3.org/2001/XMLSchema}string"/>
        + *           <element name="IconID" type="{}iconId"/>
        + *           <element name="CustomIconUUID" type="{}customIconUuidRef" minOccurs="0"/>
        + *           <element ref="{}Times"/>
        + *           <element name="IsExpanded" type="{}keepassBoolean"/>
        + *           <element name="DefaultAutoTypeSequence" type="{http://www.w3.org/2001/XMLSchema}string"/>
        + *           <element name="EnableAutoType" type="{}keepassBoolean"/>
        + *           <element name="EnableSearching" type="{}keepassBoolean"/>
        + *           <element name="LastTopVisibleEntry" type="{}uuidRef"/>
        + *           <element ref="{}Entry" maxOccurs="unbounded" minOccurs="0"/>
        + *           <element ref="{}Group" maxOccurs="unbounded" minOccurs="0"/>
        + *           <element name="CustomData" type="{}customData" minOccurs="0"/>
        + *         </sequence>
        + *       </extension>
        + *     </complexContent>
        + *   </complexType>
        + * </element>
          * 
        * * @@ -59,6 +61,7 @@ "name", "notes", "iconID", + "customIconUUID", "times", "isExpanded", "defaultAutoTypeSequence", @@ -66,7 +69,8 @@ "enableSearching", "lastTopVisibleEntry", "entry", - "group" + "group", + "customData" }) @XmlRootElement(name = "Group") public class JaxbGroupBinding @@ -83,6 +87,10 @@ public class JaxbGroupBinding protected String notes; @XmlElement(name = "IconID") protected int iconID; + @XmlElement(name = "CustomIconUUID", type = String.class) + @XmlJavaTypeAdapter(Adapter3 .class) + @XmlSchemaType(name = "base64Binary") + protected UUID customIconUUID; @XmlElement(name = "Times", required = true) protected Times times; @XmlElement(name = "IsExpanded", required = true, type = String.class) @@ -104,6 +112,8 @@ public class JaxbGroupBinding protected List entry; @XmlElement(name = "Group") protected List group; + @XmlElement(name = "CustomData") + protected CustomData customData; /** * Gets the value of the uuid property. @@ -193,6 +203,30 @@ public void setIconID(int value) { this.iconID = value; } + /** + * Gets the value of the customIconUUID property. + * + * @return + * possible object is + * {@link String } + * + */ + public UUID getCustomIconUUID() { + return customIconUUID; + } + + /** + * Sets the value of the customIconUUID property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setCustomIconUUID(UUID value) { + this.customIconUUID = value; + } + /** * Gets the value of the times property. * @@ -395,4 +429,28 @@ public List getGroup() { return this.group; } + /** + * Gets the value of the customData property. + * + * @return + * possible object is + * {@link CustomData } + * + */ + public CustomData getCustomData() { + return customData; + } + + /** + * Sets the value of the customData property. + * + * @param value + * allowed object is + * {@link CustomData } + * + */ + public void setCustomData(CustomData value) { + this.customData = value; + } + } diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/KeePassFile.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/KeePassFile.java index e5500451..f5af5a3b 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/KeePassFile.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/KeePassFile.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.16 at 04:44:10 PM BST // @@ -25,76 +25,76 @@ *

        The following schema fragment specifies the expected content contained within this class. * *

        - * <complexType>
        - *   <complexContent>
        - *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        - *       <sequence>
        - *         <element name="Meta">
        - *           <complexType>
        - *             <complexContent>
        - *               <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        - *                 <sequence>
        - *                   <element name="Generator" type="{http://www.w3.org/2001/XMLSchema}string"/>
        - *                   <element name="HeaderHash" type="{http://www.w3.org/2001/XMLSchema}base64Binary" minOccurs="0"/>
        - *                   <element name="DatabaseName" type="{http://www.w3.org/2001/XMLSchema}string"/>
        - *                   <element name="DatabaseNameChanged" type="{}keepassDateTime"/>
        - *                   <element name="DatabaseDescription" type="{http://www.w3.org/2001/XMLSchema}string"/>
        - *                   <element name="DatabaseDescriptionChanged" type="{}keepassDateTime"/>
        - *                   <element name="DefaultUserName" type="{http://www.w3.org/2001/XMLSchema}string"/>
        - *                   <element name="DefaultUserNameChanged" type="{}keepassDateTime"/>
        - *                   <element name="MaintenanceHistoryDays" type="{http://www.w3.org/2001/XMLSchema}int"/>
        - *                   <element name="Color" type="{}color"/>
        - *                   <element name="MasterKeyChanged" type="{}keepassDateTime"/>
        - *                   <element name="MasterKeyChangeRec" type="{}changeDays"/>
        - *                   <element name="MasterKeyChangeForce" type="{}changeDays"/>
        - *                   <element name="MemoryProtection">
        - *                     <complexType>
        - *                       <complexContent>
        - *                         <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        - *                           <sequence>
        - *                             <element name="ProtectTitle" type="{}keepassBoolean"/>
        - *                             <element name="ProtectUserName" type="{}keepassBoolean"/>
        - *                             <element name="ProtectPassword" type="{}keepassBoolean"/>
        - *                             <element name="ProtectURL" type="{}keepassBoolean"/>
        - *                             <element name="ProtectNotes" type="{}keepassBoolean"/>
        - *                           </sequence>
        - *                         </restriction>
        - *                       </complexContent>
        - *                     </complexType>
        - *                   </element>
        - *                   <element name="CustomIcons" type="{}customIcons"/>
        - *                   <element name="RecycleBinEnabled" type="{}keepassBoolean"/>
        - *                   <element name="RecycleBinUUID" type="{}uuidRef"/>
        - *                   <element name="RecycleBinChanged" type="{}keepassDateTime"/>
        - *                   <element name="EntryTemplatesGroup" type="{}uuidRef"/>
        - *                   <element name="EntryTemplatesGroupChanged" type="{}keepassDateTime"/>
        - *                   <element name="LastSelectedGroup" type="{}uuidRef"/>
        - *                   <element name="LastTopVisibleGroup" type="{}uuidRef"/>
        - *                   <element name="HistoryMaxItems" type="{http://www.w3.org/2001/XMLSchema}int"/>
        - *                   <element name="HistoryMaxSize" type="{http://www.w3.org/2001/XMLSchema}int"/>
        - *                   <element name="Binaries" type="{}binaries"/>
        - *                   <element name="CustomData" type="{}customData"/>
        - *                 </sequence>
        - *               </restriction>
        - *             </complexContent>
        - *           </complexType>
        - *         </element>
        - *         <element name="Root">
        - *           <complexType>
        - *             <complexContent>
        - *               <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        - *                 <sequence>
        - *                   <element ref="{}Group"/>
        - *                   <element name="DeletedObjects" type="{http://www.w3.org/2001/XMLSchema}anyType"/>
        - *                 </sequence>
        - *               </restriction>
        - *             </complexContent>
        - *           </complexType>
        - *         </element>
        - *       </sequence>
        - *     </restriction>
        - *   </complexContent>
        - * </complexType>
        + * <complexType>
        + *   <complexContent>
        + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        + *       <sequence>
        + *         <element name="Meta">
        + *           <complexType>
        + *             <complexContent>
        + *               <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        + *                 <sequence>
        + *                   <element name="Generator" type="{http://www.w3.org/2001/XMLSchema}string"/>
        + *                   <element name="HeaderHash" type="{http://www.w3.org/2001/XMLSchema}base64Binary" minOccurs="0"/>
        + *                   <element name="DatabaseName" type="{http://www.w3.org/2001/XMLSchema}string"/>
        + *                   <element name="DatabaseNameChanged" type="{}keepassDateTime"/>
        + *                   <element name="DatabaseDescription" type="{http://www.w3.org/2001/XMLSchema}string"/>
        + *                   <element name="DatabaseDescriptionChanged" type="{}keepassDateTime"/>
        + *                   <element name="DefaultUserName" type="{http://www.w3.org/2001/XMLSchema}string"/>
        + *                   <element name="DefaultUserNameChanged" type="{}keepassDateTime"/>
        + *                   <element name="MaintenanceHistoryDays" type="{http://www.w3.org/2001/XMLSchema}int"/>
        + *                   <element name="Color" type="{}color"/>
        + *                   <element name="MasterKeyChanged" type="{}keepassDateTime"/>
        + *                   <element name="MasterKeyChangeRec" type="{}changeDays"/>
        + *                   <element name="MasterKeyChangeForce" type="{}changeDays"/>
        + *                   <element name="MemoryProtection">
        + *                     <complexType>
        + *                       <complexContent>
        + *                         <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        + *                           <sequence>
        + *                             <element name="ProtectTitle" type="{}keepassBoolean"/>
        + *                             <element name="ProtectUserName" type="{}keepassBoolean"/>
        + *                             <element name="ProtectPassword" type="{}keepassBoolean"/>
        + *                             <element name="ProtectURL" type="{}keepassBoolean"/>
        + *                             <element name="ProtectNotes" type="{}keepassBoolean"/>
        + *                           </sequence>
        + *                         </restriction>
        + *                       </complexContent>
        + *                     </complexType>
        + *                   </element>
        + *                   <element name="CustomIcons" type="{}customIcons"/>
        + *                   <element name="RecycleBinEnabled" type="{}keepassBoolean"/>
        + *                   <element name="RecycleBinUUID" type="{}uuidRef"/>
        + *                   <element name="RecycleBinChanged" type="{}keepassDateTime"/>
        + *                   <element name="EntryTemplatesGroup" type="{}uuidRef"/>
        + *                   <element name="EntryTemplatesGroupChanged" type="{}keepassDateTime"/>
        + *                   <element name="LastSelectedGroup" type="{}uuidRef"/>
        + *                   <element name="LastTopVisibleGroup" type="{}uuidRef"/>
        + *                   <element name="HistoryMaxItems" type="{http://www.w3.org/2001/XMLSchema}int"/>
        + *                   <element name="HistoryMaxSize" type="{http://www.w3.org/2001/XMLSchema}int"/>
        + *                   <element name="Binaries" type="{}binaries" minOccurs="0"/>
        + *                   <element name="CustomData" type="{}customData"/>
        + *                 </sequence>
        + *               </restriction>
        + *             </complexContent>
        + *           </complexType>
        + *         </element>
        + *         <element name="Root">
        + *           <complexType>
        + *             <complexContent>
        + *               <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        + *                 <sequence>
        + *                   <element ref="{}Group"/>
        + *                   <element name="DeletedObjects" type="{http://www.w3.org/2001/XMLSchema}anyType"/>
        + *                 </sequence>
        + *               </restriction>
        + *             </complexContent>
        + *           </complexType>
        + *         </element>
        + *       </sequence>
        + *     </restriction>
        + *   </complexContent>
        + * </complexType>
          * 
        * * @@ -173,54 +173,54 @@ public void setRoot(KeePassFile.Root value) { *

        The following schema fragment specifies the expected content contained within this class. * *

        -     * <complexType>
        -     *   <complexContent>
        -     *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        -     *       <sequence>
        -     *         <element name="Generator" type="{http://www.w3.org/2001/XMLSchema}string"/>
        -     *         <element name="HeaderHash" type="{http://www.w3.org/2001/XMLSchema}base64Binary" minOccurs="0"/>
        -     *         <element name="DatabaseName" type="{http://www.w3.org/2001/XMLSchema}string"/>
        -     *         <element name="DatabaseNameChanged" type="{}keepassDateTime"/>
        -     *         <element name="DatabaseDescription" type="{http://www.w3.org/2001/XMLSchema}string"/>
        -     *         <element name="DatabaseDescriptionChanged" type="{}keepassDateTime"/>
        -     *         <element name="DefaultUserName" type="{http://www.w3.org/2001/XMLSchema}string"/>
        -     *         <element name="DefaultUserNameChanged" type="{}keepassDateTime"/>
        -     *         <element name="MaintenanceHistoryDays" type="{http://www.w3.org/2001/XMLSchema}int"/>
        -     *         <element name="Color" type="{}color"/>
        -     *         <element name="MasterKeyChanged" type="{}keepassDateTime"/>
        -     *         <element name="MasterKeyChangeRec" type="{}changeDays"/>
        -     *         <element name="MasterKeyChangeForce" type="{}changeDays"/>
        -     *         <element name="MemoryProtection">
        -     *           <complexType>
        -     *             <complexContent>
        -     *               <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        -     *                 <sequence>
        -     *                   <element name="ProtectTitle" type="{}keepassBoolean"/>
        -     *                   <element name="ProtectUserName" type="{}keepassBoolean"/>
        -     *                   <element name="ProtectPassword" type="{}keepassBoolean"/>
        -     *                   <element name="ProtectURL" type="{}keepassBoolean"/>
        -     *                   <element name="ProtectNotes" type="{}keepassBoolean"/>
        -     *                 </sequence>
        -     *               </restriction>
        -     *             </complexContent>
        -     *           </complexType>
        -     *         </element>
        -     *         <element name="CustomIcons" type="{}customIcons"/>
        -     *         <element name="RecycleBinEnabled" type="{}keepassBoolean"/>
        -     *         <element name="RecycleBinUUID" type="{}uuidRef"/>
        -     *         <element name="RecycleBinChanged" type="{}keepassDateTime"/>
        -     *         <element name="EntryTemplatesGroup" type="{}uuidRef"/>
        -     *         <element name="EntryTemplatesGroupChanged" type="{}keepassDateTime"/>
        -     *         <element name="LastSelectedGroup" type="{}uuidRef"/>
        -     *         <element name="LastTopVisibleGroup" type="{}uuidRef"/>
        -     *         <element name="HistoryMaxItems" type="{http://www.w3.org/2001/XMLSchema}int"/>
        -     *         <element name="HistoryMaxSize" type="{http://www.w3.org/2001/XMLSchema}int"/>
        -     *         <element name="Binaries" type="{}binaries"/>
        -     *         <element name="CustomData" type="{}customData"/>
        -     *       </sequence>
        -     *     </restriction>
        -     *   </complexContent>
        -     * </complexType>
        +     * <complexType>
        +     *   <complexContent>
        +     *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        +     *       <sequence>
        +     *         <element name="Generator" type="{http://www.w3.org/2001/XMLSchema}string"/>
        +     *         <element name="HeaderHash" type="{http://www.w3.org/2001/XMLSchema}base64Binary" minOccurs="0"/>
        +     *         <element name="DatabaseName" type="{http://www.w3.org/2001/XMLSchema}string"/>
        +     *         <element name="DatabaseNameChanged" type="{}keepassDateTime"/>
        +     *         <element name="DatabaseDescription" type="{http://www.w3.org/2001/XMLSchema}string"/>
        +     *         <element name="DatabaseDescriptionChanged" type="{}keepassDateTime"/>
        +     *         <element name="DefaultUserName" type="{http://www.w3.org/2001/XMLSchema}string"/>
        +     *         <element name="DefaultUserNameChanged" type="{}keepassDateTime"/>
        +     *         <element name="MaintenanceHistoryDays" type="{http://www.w3.org/2001/XMLSchema}int"/>
        +     *         <element name="Color" type="{}color"/>
        +     *         <element name="MasterKeyChanged" type="{}keepassDateTime"/>
        +     *         <element name="MasterKeyChangeRec" type="{}changeDays"/>
        +     *         <element name="MasterKeyChangeForce" type="{}changeDays"/>
        +     *         <element name="MemoryProtection">
        +     *           <complexType>
        +     *             <complexContent>
        +     *               <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        +     *                 <sequence>
        +     *                   <element name="ProtectTitle" type="{}keepassBoolean"/>
        +     *                   <element name="ProtectUserName" type="{}keepassBoolean"/>
        +     *                   <element name="ProtectPassword" type="{}keepassBoolean"/>
        +     *                   <element name="ProtectURL" type="{}keepassBoolean"/>
        +     *                   <element name="ProtectNotes" type="{}keepassBoolean"/>
        +     *                 </sequence>
        +     *               </restriction>
        +     *             </complexContent>
        +     *           </complexType>
        +     *         </element>
        +     *         <element name="CustomIcons" type="{}customIcons"/>
        +     *         <element name="RecycleBinEnabled" type="{}keepassBoolean"/>
        +     *         <element name="RecycleBinUUID" type="{}uuidRef"/>
        +     *         <element name="RecycleBinChanged" type="{}keepassDateTime"/>
        +     *         <element name="EntryTemplatesGroup" type="{}uuidRef"/>
        +     *         <element name="EntryTemplatesGroupChanged" type="{}keepassDateTime"/>
        +     *         <element name="LastSelectedGroup" type="{}uuidRef"/>
        +     *         <element name="LastTopVisibleGroup" type="{}uuidRef"/>
        +     *         <element name="HistoryMaxItems" type="{http://www.w3.org/2001/XMLSchema}int"/>
        +     *         <element name="HistoryMaxSize" type="{http://www.w3.org/2001/XMLSchema}int"/>
        +     *         <element name="Binaries" type="{}binaries" minOccurs="0"/>
        +     *         <element name="CustomData" type="{}customData"/>
        +     *       </sequence>
        +     *     </restriction>
        +     *   </complexContent>
        +     * </complexType>
              * 
        * * @@ -281,7 +281,6 @@ public static class Meta { @XmlElement(name = "MaintenanceHistoryDays") protected int maintenanceHistoryDays; @XmlElement(name = "Color", required = true) - @XmlSchemaType(name = "anySimpleType") protected String color; @XmlElement(name = "MasterKeyChanged", required = true, type = String.class) @XmlJavaTypeAdapter(Adapter1 .class) @@ -326,7 +325,7 @@ public static class Meta { protected int historyMaxItems; @XmlElement(name = "HistoryMaxSize") protected int historyMaxSize; - @XmlElement(name = "Binaries", required = true) + @XmlElement(name = "Binaries") protected Binaries binaries; @XmlElement(name = "CustomData", required = true) protected CustomData customData; @@ -920,19 +919,19 @@ public void setCustomData(CustomData value) { *

        The following schema fragment specifies the expected content contained within this class. * *

        -         * <complexType>
        -         *   <complexContent>
        -         *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        -         *       <sequence>
        -         *         <element name="ProtectTitle" type="{}keepassBoolean"/>
        -         *         <element name="ProtectUserName" type="{}keepassBoolean"/>
        -         *         <element name="ProtectPassword" type="{}keepassBoolean"/>
        -         *         <element name="ProtectURL" type="{}keepassBoolean"/>
        -         *         <element name="ProtectNotes" type="{}keepassBoolean"/>
        -         *       </sequence>
        -         *     </restriction>
        -         *   </complexContent>
        -         * </complexType>
        +         * <complexType>
        +         *   <complexContent>
        +         *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        +         *       <sequence>
        +         *         <element name="ProtectTitle" type="{}keepassBoolean"/>
        +         *         <element name="ProtectUserName" type="{}keepassBoolean"/>
        +         *         <element name="ProtectPassword" type="{}keepassBoolean"/>
        +         *         <element name="ProtectURL" type="{}keepassBoolean"/>
        +         *         <element name="ProtectNotes" type="{}keepassBoolean"/>
        +         *       </sequence>
        +         *     </restriction>
        +         *   </complexContent>
        +         * </complexType>
                  * 
        * * @@ -1094,16 +1093,16 @@ public void setProtectNotes(Boolean value) { *

        The following schema fragment specifies the expected content contained within this class. * *

        -     * <complexType>
        -     *   <complexContent>
        -     *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        -     *       <sequence>
        -     *         <element ref="{}Group"/>
        -     *         <element name="DeletedObjects" type="{http://www.w3.org/2001/XMLSchema}anyType"/>
        -     *       </sequence>
        -     *     </restriction>
        -     *   </complexContent>
        -     * </complexType>
        +     * <complexType>
        +     *   <complexContent>
        +     *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        +     *       <sequence>
        +     *         <element ref="{}Group"/>
        +     *         <element name="DeletedObjects" type="{http://www.w3.org/2001/XMLSchema}anyType"/>
        +     *       </sequence>
        +     *     </restriction>
        +     *   </complexContent>
        +     * </complexType>
              * 
        * * diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ObjectFactory.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ObjectFactory.java index da7b2e71..894158f2 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ObjectFactory.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ObjectFactory.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.16 at 04:44:10 PM BST // @@ -58,19 +58,19 @@ public Binaries createBinaries() { } /** - * Create an instance of {@link AutoType } + * Create an instance of {@link StringField } * */ - public AutoType createAutoType() { - return new AutoType(); + public StringField createStringField() { + return new StringField(); } /** - * Create an instance of {@link CustomIcons } + * Create an instance of {@link AutoType } * */ - public CustomIcons createCustomIcons() { - return new CustomIcons(); + public AutoType createAutoType() { + return new AutoType(); } /** @@ -82,11 +82,11 @@ public BinaryField createBinaryField() { } /** - * Create an instance of {@link StringField } + * Create an instance of {@link CustomIcons } * */ - public StringField createStringField() { - return new StringField(); + public CustomIcons createCustomIcons() { + return new CustomIcons(); } /** @@ -97,14 +97,6 @@ public KeePassFile.Meta createKeePassFileMeta() { return new KeePassFile.Meta(); } - /** - * Create an instance of {@link KeePassFile.Root } - * - */ - public KeePassFile.Root createKeePassFileRoot() { - return new KeePassFile.Root(); - } - /** * Create an instance of {@link JaxbGroupBinding } * @@ -137,6 +129,14 @@ public History createHistory() { return new History(); } + /** + * Create an instance of {@link KeePassFile.Root } + * + */ + public KeePassFile.Root createKeePassFileRoot() { + return new KeePassFile.Root(); + } + /** * Create an instance of {@link CustomData } * @@ -154,19 +154,19 @@ public Binaries.Binary createBinariesBinary() { } /** - * Create an instance of {@link AutoType.Association } + * Create an instance of {@link StringField.Value } * */ - public AutoType.Association createAutoTypeAssociation() { - return new AutoType.Association(); + public StringField.Value createStringFieldValue() { + return new StringField.Value(); } /** - * Create an instance of {@link CustomIcons.Icon } + * Create an instance of {@link AutoType.Association } * */ - public CustomIcons.Icon createCustomIconsIcon() { - return new CustomIcons.Icon(); + public AutoType.Association createAutoTypeAssociation() { + return new AutoType.Association(); } /** @@ -178,11 +178,11 @@ public BinaryField.Value createBinaryFieldValue() { } /** - * Create an instance of {@link StringField.Value } + * Create an instance of {@link CustomIcons.Icon } * */ - public StringField.Value createStringFieldValue() { - return new StringField.Value(); + public CustomIcons.Icon createCustomIconsIcon() { + return new CustomIcons.Icon(); } /** diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Parented.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Parented.java index a78246e7..b69bf42e 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Parented.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Parented.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.16 at 04:44:10 PM BST // @@ -15,7 +15,9 @@ /** - * This class, while generated, is not used see {@link org.linguafranca.pwdb.kdbx.jaxb.base.AbstractJaxbParentedBinding} which is the actual implementation class. + * This class, while generated, is not used see {@link + * org.linguafranca.pwdb.kdbx.jaxb.org.linguafranca.pwdb.base.AbstractJaxbParentedBinding} which is the + * actual implementation class. * * * diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/StringField.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/StringField.java index 4414480a..fd0bfcff 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/StringField.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/StringField.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.16 at 04:44:10 PM BST // @@ -13,15 +13,15 @@ import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlType; -import javax.xml.bind.annotation.XmlValue; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import org.linguafranca.pwdb.kdbx.jaxb.base.ValueBinding; /** * This is where the values of the database are actually stored. You can have String valued - * fields and you can have Binary valued fields. There are "Default" String fields (username and so on) and + * fields, and you can have Binary valued fields. There are "Default" String fields (username and so on) and * there are custom string fields (custom only in that their names are not the names of default string - * fields. Not really clear whether the keys are case sensitive. + * fields). Not really clear whether the keys are case-sensitive. * * *

        Java class for stringField complex type. @@ -29,25 +29,25 @@ *

        The following schema fragment specifies the expected content contained within this class. * *

        - * <complexType name="stringField">
        - *   <complexContent>
        - *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        - *       <sequence>
        - *         <element name="Key" type="{http://www.w3.org/2001/XMLSchema}string"/>
        - *         <element name="Value">
        - *           <complexType>
        - *             <simpleContent>
        - *               <extension base="<http://www.w3.org/2001/XMLSchema>string">
        - *                 <attribute name="Protected" type="{}keepassBoolean" />
        - *                 <attribute name="ProtectInMemory" type="{}keepassBoolean" />
        - *               </extension>
        - *             </simpleContent>
        - *           </complexType>
        - *         </element>
        - *       </sequence>
        - *     </restriction>
        - *   </complexContent>
        - * </complexType>
        + * <complexType name="stringField">
        + *   <complexContent>
        + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        + *       <sequence>
        + *         <element name="Key" type="{http://www.w3.org/2001/XMLSchema}string"/>
        + *         <element name="Value">
        + *           <complexType>
        + *             <simpleContent>
        + *               <extension base="<>ValueExtender">
        + *                 <attribute name="Protected" type="{}keepassBoolean" default="False" />
        + *                 <attribute name="ProtectInMemory" type="{}keepassBoolean" default="False" />
        + *               </extension>
        + *             </simpleContent>
        + *           </complexType>
        + *         </element>
        + *       </sequence>
        + *     </restriction>
        + *   </complexContent>
        + * </complexType>
          * 
        * * @@ -119,26 +119,24 @@ public void setValue(StringField.Value value) { *

        The following schema fragment specifies the expected content contained within this class. * *

        -     * <complexType>
        -     *   <simpleContent>
        -     *     <extension base="<http://www.w3.org/2001/XMLSchema>string">
        -     *       <attribute name="Protected" type="{}keepassBoolean" />
        -     *       <attribute name="ProtectInMemory" type="{}keepassBoolean" />
        -     *     </extension>
        -     *   </simpleContent>
        -     * </complexType>
        +     * <complexType>
        +     *   <simpleContent>
        +     *     <extension base="<>ValueExtender">
        +     *       <attribute name="Protected" type="{}keepassBoolean" default="False" />
        +     *       <attribute name="ProtectInMemory" type="{}keepassBoolean" default="False" />
        +     *     </extension>
        +     *   </simpleContent>
        +     * </complexType>
              * 
        * * */ @XmlAccessorType(XmlAccessType.FIELD) - @XmlType(name = "", propOrder = { - "value" - }) - public static class Value { + @XmlType(name = "") + public static class Value + extends ValueBinding + { - @XmlValue - protected String value; @XmlAttribute(name = "Protected") @XmlJavaTypeAdapter(Adapter2 .class) protected Boolean _protected; @@ -146,30 +144,6 @@ public static class Value { @XmlJavaTypeAdapter(Adapter2 .class) protected Boolean protectInMemory; - /** - * Gets the value of the value property. - * - * @return - * possible object is - * {@link String } - * - */ - public String getValue() { - return value; - } - - /** - * Sets the value of the value property. - * - * @param value - * allowed object is - * {@link String } - * - */ - public void setValue(String value) { - this.value = value; - } - /** * Gets the value of the protected property. * @@ -179,7 +153,11 @@ public void setValue(String value) { * */ public Boolean getProtected() { - return _protected; + if (_protected == null) { + return new Adapter2().unmarshal("False"); + } else { + return _protected; + } } /** @@ -203,7 +181,11 @@ public void setProtected(Boolean value) { * */ public Boolean getProtectInMemory() { - return protectInMemory; + if (protectInMemory == null) { + return new Adapter2().unmarshal("False"); + } else { + return protectInMemory; + } } /** diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Times.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Times.java index f45f2ec8..28b3b247 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Times.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Times.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.16 at 04:44:10 PM BST // @@ -24,21 +24,21 @@ *

        The following schema fragment specifies the expected content contained within this class. * *

        - * <complexType>
        - *   <complexContent>
        - *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        - *       <sequence>
        - *         <element name="LastModificationTime" type="{}keepassDateTime"/>
        - *         <element name="CreationTime" type="{}keepassDateTime"/>
        - *         <element name="LastAccessTime" type="{}keepassDateTime"/>
        - *         <element name="ExpiryTime" type="{}keepassDateTime"/>
        - *         <element name="Expires" type="{}keepassBoolean"/>
        - *         <element name="UsageCount" type="{http://www.w3.org/2001/XMLSchema}int"/>
        - *         <element name="LocationChanged" type="{}keepassDateTime"/>
        - *       </sequence>
        - *     </restriction>
        - *   </complexContent>
        - * </complexType>
        + * <complexType>
        + *   <complexContent>
        + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
        + *       <sequence>
        + *         <element name="LastModificationTime" type="{}keepassDateTime"/>
        + *         <element name="CreationTime" type="{}keepassDateTime"/>
        + *         <element name="LastAccessTime" type="{}keepassDateTime"/>
        + *         <element name="ExpiryTime" type="{}keepassDateTime"/>
        + *         <element name="Expires" type="{}keepassBoolean"/>
        + *         <element name="UsageCount" type="{http://www.w3.org/2001/XMLSchema}int"/>
        + *         <element name="LocationChanged" type="{}keepassDateTime"/>
        + *       </sequence>
        + *     </restriction>
        + *   </complexContent>
        + * </complexType>
          * 
        * * diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ValueExtender.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ValueExtender.java new file mode 100644 index 00000000..9fc950b8 --- /dev/null +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ValueExtender.java @@ -0,0 +1,70 @@ +// +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 +// See http://java.sun.com/xml/jaxb +// Any modifications to this file will be lost upon recompilation of the source schema. +// Generated on: 2023.05.16 at 04:44:10 PM BST +// + + +package org.linguafranca.pwdb.kdbx.jaxb.binding; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlSeeAlso; +import javax.xml.bind.annotation.XmlType; +import javax.xml.bind.annotation.XmlValue; + + +/** + *

        Java class for ValueExtender complex type. + * + *

        The following schema fragment specifies the expected content contained within this class. + * + *

        + * <complexType name="ValueExtender">
        + *   <simpleContent>
        + *     <extension base="<http://www.w3.org/2001/XMLSchema>string">
        + *     </extension>
        + *   </simpleContent>
        + * </complexType>
        + * 
        + * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "ValueExtender", propOrder = { + "value" +}) +@XmlSeeAlso({ + org.linguafranca.pwdb.kdbx.jaxb.binding.StringField.Value.class +}) +public abstract class ValueExtender { + + @XmlValue + protected String value; + + /** + * Gets the value of the value property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getValue() { + return value; + } + + /** + * Sets the value of the value property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setValue(String value) { + this.value = value; + } + +} diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java index 81b7697d..56533212 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java @@ -21,6 +21,7 @@ import org.jetbrains.annotations.Nullable; import org.linguafranca.pwdb.SerializableDatabase; import org.linguafranca.pwdb.kdbx.Helpers; +import org.linguafranca.pwdb.kdbx.jaxb.base.ValueBinding; import org.linguafranca.pwdb.kdbx.jaxb.binding.*; import org.linguafranca.pwdb.kdbx.dom.DomHelper; import org.linguafranca.pwdb.security.StreamEncryptor; @@ -84,7 +85,7 @@ public static void addBinary(KeePassFile keePassFile, ObjectFactory objectFactor @Override public JaxbSerializableDatabase load(InputStream inputStream) { try { - JAXBContext jc = JAXBContext.newInstance(KeePassFile.class); + JAXBContext jc = JAXBContext.newInstance(KeePassFile.class, ValueBinding.class); Unmarshaller u = jc.createUnmarshaller(); u.setListener(new Unmarshaller.Listener() { @Override diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/base/ValueBinding.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/base/ValueBinding.java new file mode 100644 index 00000000..9b87f02a --- /dev/null +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/base/ValueBinding.java @@ -0,0 +1,24 @@ +package org.linguafranca.pwdb.kdbx.jaxb.base; + +import javax.xml.bind.annotation.XmlTransient; +import javax.xml.bind.annotation.XmlValue; + +/** + * providing a way of flagging protection + */ +public abstract class ValueBinding { + + @XmlTransient + public boolean protectOnOutput; + + protected String value; + + public String getValue(){ + return value; + } + + public void setValue(String string){ + value = string; + } + +} From bebad3c34205ce31de7d9d44ed3692c9a1c78f52 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Wed, 17 May 2023 11:33:52 +0100 Subject: [PATCH 118/228] JAXB Value marshal and unmarshal now working with custom Value implementation --- KDBX.4.xsd | 8 +++ jaxb/bindings.xml | 1 - .../pwdb/kdbx/jaxb/binding/Adapter1.java | 2 +- .../pwdb/kdbx/jaxb/binding/Adapter2.java | 2 +- .../pwdb/kdbx/jaxb/binding/Adapter3.java | 2 +- .../pwdb/kdbx/jaxb/binding/AutoType.java | 2 +- .../pwdb/kdbx/jaxb/binding/Binaries.java | 2 +- .../pwdb/kdbx/jaxb/binding/BinaryField.java | 2 +- .../pwdb/kdbx/jaxb/binding/CustomData.java | 2 +- .../pwdb/kdbx/jaxb/binding/CustomIcons.java | 63 ++++++++++++++++++- .../pwdb/kdbx/jaxb/binding/History.java | 2 +- .../kdbx/jaxb/binding/JaxbEntryBinding.java | 2 +- .../kdbx/jaxb/binding/JaxbGroupBinding.java | 2 +- .../pwdb/kdbx/jaxb/binding/KeePassFile.java | 2 +- .../pwdb/kdbx/jaxb/binding/ObjectFactory.java | 2 +- .../pwdb/kdbx/jaxb/binding/Parented.java | 2 +- .../pwdb/kdbx/jaxb/binding/StringField.java | 2 +- .../pwdb/kdbx/jaxb/binding/Times.java | 2 +- .../pwdb/kdbx/jaxb/binding/ValueExtender.java | 7 ++- .../pwdb/kdbx/jaxb/base/ValueBinding.java | 9 ++- .../pwdb/kdbx/jaxb/util/DateAdapter.java | 1 + 21 files changed, 99 insertions(+), 20 deletions(-) diff --git a/KDBX.4.xsd b/KDBX.4.xsd index be556bcf..4fb7d70a 100644 --- a/KDBX.4.xsd +++ b/KDBX.4.xsd @@ -321,6 +321,12 @@ + + Abstract parent for StringField/Value so that + to allow creation of a custom mapping that contains extra + non-serialised (transient) fields. + + @@ -409,6 +415,8 @@ + + diff --git a/jaxb/bindings.xml b/jaxb/bindings.xml index 75693d85..c6907af8 100644 --- a/jaxb/bindings.xml +++ b/jaxb/bindings.xml @@ -39,7 +39,6 @@ /> - diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter1.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter1.java index dd791bf6..16005b3b 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter1.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter1.java @@ -2,7 +2,7 @@ // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2023.05.16 at 04:44:10 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter2.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter2.java index 947683fb..530b9b09 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter2.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter2.java @@ -2,7 +2,7 @@ // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2023.05.16 at 04:44:10 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter3.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter3.java index c2b9ef09..aa3295be 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter3.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter3.java @@ -2,7 +2,7 @@ // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2023.05.16 at 04:44:10 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/AutoType.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/AutoType.java index ad14484b..fe87bdea 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/AutoType.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/AutoType.java @@ -2,7 +2,7 @@ // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2023.05.16 at 04:44:10 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Binaries.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Binaries.java index 6ec3ce9d..4090b908 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Binaries.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Binaries.java @@ -2,7 +2,7 @@ // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2023.05.16 at 04:44:10 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/BinaryField.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/BinaryField.java index 045583c4..50581d10 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/BinaryField.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/BinaryField.java @@ -2,7 +2,7 @@ // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2023.05.16 at 04:44:10 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomData.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomData.java index 43f2c041..cdd94c65 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomData.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomData.java @@ -2,7 +2,7 @@ // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2023.05.16 at 04:44:10 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomIcons.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomIcons.java index eff8ee36..aa4b00d8 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomIcons.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomIcons.java @@ -2,13 +2,14 @@ // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2023.05.16 at 04:44:10 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // package org.linguafranca.pwdb.kdbx.jaxb.binding; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.UUID; import javax.xml.bind.annotation.XmlAccessType; @@ -35,6 +36,8 @@ * <restriction base="{http://www.w3.org/2001/XMLSchema}anyType"> * <sequence> * <element name="UUID" type="{}uuid"/> + * <element name="Name" type="{http://www.w3.org/2001/XMLSchema}string"/> + * <element name="LastModificationTime" type="{}keepassDateTime"/> * <element name="Data" type="{http://www.w3.org/2001/XMLSchema}base64Binary"/> * </sequence> * </restriction> @@ -99,6 +102,8 @@ public List getIcon() { * <restriction base="{http://www.w3.org/2001/XMLSchema}anyType"> * <sequence> * <element name="UUID" type="{}uuid"/> + * <element name="Name" type="{http://www.w3.org/2001/XMLSchema}string"/> + * <element name="LastModificationTime" type="{}keepassDateTime"/> * <element name="Data" type="{http://www.w3.org/2001/XMLSchema}base64Binary"/> * </sequence> * </restriction> @@ -111,6 +116,8 @@ public List getIcon() { @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "", propOrder = { "uuid", + "name", + "lastModificationTime", "data" }) public static class Icon { @@ -119,6 +126,12 @@ public static class Icon { @XmlJavaTypeAdapter(Adapter3 .class) @XmlSchemaType(name = "base64Binary") protected UUID uuid; + @XmlElement(name = "Name", required = true) + protected String name; + @XmlElement(name = "LastModificationTime", required = true, type = String.class) + @XmlJavaTypeAdapter(Adapter1 .class) + @XmlSchemaType(name = "dateTime") + protected Date lastModificationTime; @XmlElement(name = "Data", required = true) protected byte[] data; @@ -146,6 +159,54 @@ public void setUUID(UUID value) { this.uuid = value; } + /** + * Gets the value of the name property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getName() { + return name; + } + + /** + * Sets the value of the name property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setName(String value) { + this.name = value; + } + + /** + * Gets the value of the lastModificationTime property. + * + * @return + * possible object is + * {@link String } + * + */ + public Date getLastModificationTime() { + return lastModificationTime; + } + + /** + * Sets the value of the lastModificationTime property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setLastModificationTime(Date value) { + this.lastModificationTime = value; + } + /** * Gets the value of the data property. * diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/History.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/History.java index 2db87395..d8110ad0 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/History.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/History.java @@ -2,7 +2,7 @@ // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2023.05.16 at 04:44:10 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/JaxbEntryBinding.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/JaxbEntryBinding.java index 22edbbbc..0f6d2c88 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/JaxbEntryBinding.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/JaxbEntryBinding.java @@ -2,7 +2,7 @@ // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2023.05.16 at 04:44:10 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/JaxbGroupBinding.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/JaxbGroupBinding.java index dfedbbf2..43c74da0 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/JaxbGroupBinding.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/JaxbGroupBinding.java @@ -2,7 +2,7 @@ // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2023.05.16 at 04:44:10 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/KeePassFile.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/KeePassFile.java index f5af5a3b..7b2ec424 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/KeePassFile.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/KeePassFile.java @@ -2,7 +2,7 @@ // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2023.05.16 at 04:44:10 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ObjectFactory.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ObjectFactory.java index 894158f2..0c3cf1e2 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ObjectFactory.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ObjectFactory.java @@ -2,7 +2,7 @@ // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2023.05.16 at 04:44:10 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Parented.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Parented.java index b69bf42e..331b6222 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Parented.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Parented.java @@ -2,7 +2,7 @@ // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2023.05.16 at 04:44:10 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/StringField.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/StringField.java index fd0bfcff..887ec91e 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/StringField.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/StringField.java @@ -2,7 +2,7 @@ // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2023.05.16 at 04:44:10 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Times.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Times.java index 28b3b247..2d371477 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Times.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Times.java @@ -2,7 +2,7 @@ // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2023.05.16 at 04:44:10 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ValueExtender.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ValueExtender.java index 9fc950b8..df8407c8 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ValueExtender.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ValueExtender.java @@ -2,7 +2,7 @@ // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2023.05.16 at 04:44:10 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // @@ -16,6 +16,11 @@ /** + * Abstract parent for StringField/Value so that + * to allow creation of a custom mapping that contains extra + * non-serialised (transient) fields. + * + * *

        Java class for ValueExtender complex type. * *

        The following schema fragment specifies the expected content contained within this class. diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/base/ValueBinding.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/base/ValueBinding.java index 9b87f02a..a89fc2a2 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/base/ValueBinding.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/base/ValueBinding.java @@ -1,17 +1,22 @@ package org.linguafranca.pwdb.kdbx.jaxb.base; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlTransient; import javax.xml.bind.annotation.XmlValue; /** - * providing a way of flagging protection + * Ancestor class of StringField.Value, providing a way of flagging protection + * separate from @Protected and @ProtectInMemory */ +@XmlAccessorType(XmlAccessType.FIELD) public abstract class ValueBinding { @XmlTransient public boolean protectOnOutput; - protected String value; + @XmlValue + public String value; public String getValue(){ return value; diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/util/DateAdapter.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/util/DateAdapter.java index 33c6bbd6..da5f78b3 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/util/DateAdapter.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/util/DateAdapter.java @@ -30,6 +30,7 @@ public static String toString(Date date) { } public static Date fromString(String string) { + // this is found in the base template if (string.equals("${creationDate}")) { return new Date(); } From c3999d261a2add332361ad7514ceb98dc94783e1 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Wed, 17 May 2023 21:50:58 +0100 Subject: [PATCH 119/228] Tidy up JaxbSerializableDatabase --- jaxb/pom.xml | 1 - .../kdbx/jaxb/JaxbSerializableDatabase.java | 129 ++++++++++-------- .../pwdb/kdbx/jaxb/util/JaxbUtil.java | 28 ++++ 3 files changed, 100 insertions(+), 58 deletions(-) create mode 100644 jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/util/JaxbUtil.java diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 95515f38..5b311502 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -43,7 +43,6 @@ com.sun.xml.bind jaxb-impl 2.3.3 - runtime org.linguafranca.pwdb diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java index 56533212..51cfed3f 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java @@ -16,35 +16,21 @@ package org.linguafranca.pwdb.kdbx.jaxb; +import com.sun.xml.txw2.output.IndentingXMLStreamWriter; import org.apache.commons.codec.binary.Base64; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.linguafranca.pwdb.SerializableDatabase; import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.jaxb.base.ValueBinding; import org.linguafranca.pwdb.kdbx.jaxb.binding.*; -import org.linguafranca.pwdb.kdbx.dom.DomHelper; import org.linguafranca.pwdb.security.StreamEncryptor; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.OutputKeys; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMResult; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathExpressionException; -import java.io.IOException; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; @@ -97,7 +83,7 @@ public void afterUnmarshal(Object target, Object parent) { String decrypted = new String(encryption.decrypt(encrypted), StandardCharsets.UTF_8); value.setValue(decrypted); value.setProtected(null); - value.setProtectInMemory(true); + value.protectOnOutput=true; } } if (target instanceof JaxbGroupBinding && (parent instanceof JaxbGroupBinding)) { @@ -116,7 +102,7 @@ public void afterUnmarshal(Object target, Object parent) { } @Override - public void save(OutputStream outputStream) throws IOException { + public void save(OutputStream outputStream) { final List toEncrypt = new ArrayList<>(); if (keePassFile.getMeta().getMemoryProtection().getProtectTitle()) { toEncrypt.add(org.linguafranca.pwdb.Entry.STANDARD_PROPERTY_NAME_TITLE); @@ -133,52 +119,81 @@ public void save(OutputStream outputStream) throws IOException { if (keePassFile.getMeta().getMemoryProtection().getProtectNotes()) { toEncrypt.add(org.linguafranca.pwdb.Entry.STANDARD_PROPERTY_NAME_NOTES); } - try { - // Create the Document - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - DocumentBuilder db = dbf.newDocumentBuilder(); - Document document = db.newDocument(); + try { JAXBContext jc = JAXBContext.newInstance(KeePassFile.class); Marshaller marshaller = jc.createMarshaller(); - marshaller.marshal(keePassFile, document); - - // encrypt and base64 every element marked as protected - NodeList protectedContent = (NodeList) DomHelper.xpath.evaluate("//Value", document, XPathConstants.NODESET); - for (int i = 0; i < protectedContent.getLength(); i++){ - Element element = ((Element) protectedContent.item(i)); - boolean protect = element.getAttribute("ProtectInMemory").equalsIgnoreCase("true"); - if (protect) { - String decrypted = DomHelper.getElementContent(".", element); - if (decrypted == null) { - decrypted = ""; + marshaller.setListener(new Marshaller.Listener() { + String savedValue=""; + + // this changes the content on save, so we need to change it back again, see comment below + @Override + public void beforeMarshal(Object source) { + if (source instanceof StringField) { + StringField field = (StringField) source; + if (toEncrypt.contains(field.getKey()) || field.getValue().protectOnOutput) { + savedValue = field.getValue().getValue(); + byte [] encrypted = encryption.encrypt(field.getValue().getValue().getBytes(StandardCharsets.UTF_8)); + byte [] base64Encoded = Base64.encodeBase64(encrypted); + field.getValue().setValue(new String(base64Encoded)); + field.getValue().setProtected(true); + } else { + field.getValue().setProtected(false); + } + field.getValue().setProtectInMemory(false); + } + } + + // turns out that undoing the content change we made in beforeMarshal is the easiest way of doing this + // after a couple of days of looking at it. Making a clone before serialization + // is not practical and creating an adapter is not practical either, believe me, I tried. + // That said, if you are a JAXB whizz, and you know better ... + @Override + public void afterMarshal(Object source) { + if (source instanceof StringField) { + StringField field = (StringField) source; + if (field.getValue().getProtected()) { + field.getValue().setValue(savedValue); + field.getValue().setProtected(false); + } + } + } + }); + + XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newInstance(); + XMLStreamWriter xmlStreamWriter = xmlOutputFactory.createXMLStreamWriter(outputStream); + + // this tidies up output of boolean ="False" attributes and indentation, which is incorrect + // in marshaller and also uses tabs rather than spaces, which is more economical + IndentingXMLStreamWriter writer = new IndentingXMLStreamWriter(xmlStreamWriter){ + + @Override + public void writeStartDocument() throws XMLStreamException { + setIndentStep("\t"); + super.writeStartDocument(); + } + + @Override + public void writeAttribute(String localName, String value) throws XMLStreamException { + if (localName.equals("ProtectInMemory")) { + return; + } + if (localName.equals("Protected")) { + if (!value.equalsIgnoreCase("true")){ + return; + } } - byte[] encrypted = encryption.encrypt(decrypted.getBytes()); - // Android compatibility - String base64 = new String(Base64.encodeBase64(encrypted)); - DomHelper.setElementContent(".", element, base64); - element.setAttribute("Protected", "True"); - } else { - element.removeAttribute("Protected"); + super.writeAttribute(localName, value); } - element.removeAttribute("ProtectInMemory"); - } - - try { - Transformer transformer = TransformerFactory.newInstance().newTransformer(); - transformer.setOutputProperty(OutputKeys.INDENT, "yes"); - transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); - transformer.transform(new DOMSource(document), new StreamResult(outputStream)); - } catch (Exception e) { - throw new IllegalStateException(e); - } - } catch (JAXBException | XPathExpressionException e) { + }; + marshaller.marshal(keePassFile, writer); + + } catch (Exception e) { throw new IllegalStateException(e); - } catch (ParserConfigurationException e) { - throw new RuntimeException(e); } } + @Override public StreamEncryptor getEncryption() { return encryption; diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/util/JaxbUtil.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/util/JaxbUtil.java new file mode 100644 index 00000000..7eb700f1 --- /dev/null +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/util/JaxbUtil.java @@ -0,0 +1,28 @@ +package org.linguafranca.pwdb.kdbx.jaxb.util; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.util.JAXBSource; +import javax.xml.namespace.QName; + +public class JaxbUtil { + + // https://stackoverflow.com/questions/879453/how-to-make-a-deep-copy-of-jaxb-object-like-xmlbean-xmlobject-copy + // however it does not copy transients which we need it to, so we don't use it + public static T deepCopyJAXB(T object, Class clazz) { + try { + JAXBContext jaxbContext = JAXBContext.newInstance(clazz); + JAXBElement contentObject = new JAXBElement(new QName(clazz.getSimpleName()), clazz, object); + JAXBSource source = new JAXBSource(jaxbContext, contentObject); + return jaxbContext.createUnmarshaller().unmarshal(source, clazz).getValue(); + } catch (JAXBException e) { + throw new RuntimeException(e); + } + } + + public static T deepCopyJAXB(T object) { + if(object==null) throw new RuntimeException("Can't guess at class"); + return deepCopyJAXB(object, (Class) object.getClass()); + } +} From ca68e3ec9afa44eadd37d1ad8a8fc0d086853ea5 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Thu, 18 May 2023 12:58:10 +0100 Subject: [PATCH 120/228] major refactor of SimpleDatabase to simplify and improve load and save --- .../pwdb/kdbx/simple/SimpleDatabase.java | 4 +- .../simple/SimpleSerializableDatabase.java | 20 +-- .../kdbx/simple/converter/ValueConverter.java | 68 +++++++++ .../pwdb/kdbx/simple/model/EntryClasses.java | 20 ++- .../transformer/KdbxInputTransformer.java | 97 ------------ .../transformer/KdbxOutputTransformer.java | 110 -------------- .../linguafranca/xml/XmlEventTransformer.java | 37 ----- .../xml/XmlInputStreamFilter.java | 139 ------------------ .../xml/XmlOutputStreamFilter.java | 109 -------------- .../pwdb/kdbx/KdbxInnerInputStreamTest.java | 50 ------- .../pwdb/kdbx/KdbxInnerOutputStreamTest.java | 67 --------- .../pwdb/checks/SaveAndReloadChecks.java | 2 +- 12 files changed, 97 insertions(+), 626 deletions(-) create mode 100644 simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/converter/ValueConverter.java delete mode 100644 simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxInputTransformer.java delete mode 100644 simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxOutputTransformer.java delete mode 100644 simple/src/main/java/org/linguafranca/xml/XmlEventTransformer.java delete mode 100644 simple/src/main/java/org/linguafranca/xml/XmlInputStreamFilter.java delete mode 100644 simple/src/main/java/org/linguafranca/xml/XmlOutputStreamFilter.java delete mode 100644 simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java delete mode 100644 simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerOutputStreamTest.java diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java index 0f42e849..e1685e8f 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java @@ -23,6 +23,8 @@ import org.linguafranca.pwdb.kdbx.KdbxHeader; import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import org.linguafranca.pwdb.kdbx.simple.model.KeePassFile; +import org.linguafranca.pwdb.security.Encryption; +import org.linguafranca.pwdb.security.StreamEncryptor; import java.io.IOException; import java.io.InputStream; @@ -71,7 +73,7 @@ public SimpleDatabase(KeePassFile file, StreamFormat streamFormat) { * @throws Exception on load failure */ public static SimpleDatabase loadXml(InputStream inputStream) throws Exception { - KeePassFile result = getSerializer().read(KeePassFile.class, inputStream); + KeePassFile result = getSerializer(new StreamEncryptor.None()).read(KeePassFile.class, inputStream); result.root.group.uuid = UUID.randomUUID(); return new SimpleDatabase(result, null); } diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleSerializableDatabase.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleSerializableDatabase.java index 1110dc7a..a9d4de14 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleSerializableDatabase.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleSerializableDatabase.java @@ -19,13 +19,10 @@ import org.linguafranca.pwdb.SerializableDatabase; import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.simple.converter.EmptyStringConverter; +import org.linguafranca.pwdb.kdbx.simple.converter.ValueConverter; import org.linguafranca.pwdb.kdbx.simple.model.EntryClasses; import org.linguafranca.pwdb.kdbx.simple.model.KeePassFile; -import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxInputTransformer; -import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxOutputTransformer; import org.linguafranca.pwdb.security.StreamEncryptor; -import org.linguafranca.xml.XmlInputStreamFilter; -import org.linguafranca.xml.XmlOutputStreamFilter; import org.simpleframework.xml.Serializer; import org.simpleframework.xml.convert.AnnotationStrategy; import org.simpleframework.xml.convert.Registry; @@ -63,7 +60,7 @@ public SimpleSerializableDatabase(KeePassFile keePassFile) { static KeePassFile createEmptyDatabase() { InputStream inputStream = SimpleDatabase.class.getClassLoader().getResourceAsStream("base.kdbx.xml"); try { - return getSerializer().read(KeePassFile.class, inputStream); + return getSerializer(new StreamEncryptor.None()).read(KeePassFile.class, inputStream); } catch (Exception e) { throw new RuntimeException(e); } @@ -73,9 +70,9 @@ static KeePassFile createEmptyDatabase() { public SimpleSerializableDatabase load(InputStream inputStream) { try { // decrypt the encrypted fields in the inner XML stream - InputStream plainTextXmlStream = new XmlInputStreamFilter(inputStream, new KdbxInputTransformer(encryption)); + // InputStream plainTextXmlStream = new XmlInputStreamFilter(inputStream, new KdbxInputTransformer(encryption)); // read the now entirely decrypted stream into database - keePassFile = getSerializer().read(KeePassFile.class, plainTextXmlStream); + keePassFile = getSerializer(encryption).read(KeePassFile.class, inputStream); // ensure that parent fields are set fixUp(keePassFile.root.group); return this; @@ -87,29 +84,28 @@ public SimpleSerializableDatabase load(InputStream inputStream) { @Override public void save(OutputStream outputStream) throws IOException { // encrypt the fields in the XML inner stream - XmlOutputStreamFilter plainTextOutputStream = new XmlOutputStreamFilter(outputStream, new KdbxOutputTransformer(encryption)); + // XmlOutputStreamFilter plainTextOutputStream = new XmlOutputStreamFilter(outputStream, new KdbxOutputTransformer(encryption)); // set up the "protected" attributes of fields that need inner stream encryption prepareForSave(keePassFile.root.group); // and save the database out try { - getSerializer().write(this.keePassFile, plainTextOutputStream); + getSerializer(encryption).write(this.keePassFile, outputStream); } catch (Exception e) { throw new IOException(e); } - plainTextOutputStream.close(); - plainTextOutputStream.await(); } /** * Utility to get a simple framework persister * @return a persister */ - static Serializer getSerializer() { + public static Serializer getSerializer(StreamEncryptor encryption) { Registry registry = new Registry(); try { registry.bind(String.class, EmptyStringConverter.class); + registry.bind(EntryClasses.StringProperty.Value.class, new ValueConverter(encryption)); } catch (Exception e) { throw new IllegalStateException(e); } diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/converter/ValueConverter.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/converter/ValueConverter.java new file mode 100644 index 00000000..eb436fae --- /dev/null +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/converter/ValueConverter.java @@ -0,0 +1,68 @@ +package org.linguafranca.pwdb.kdbx.simple.converter; + +import org.apache.commons.codec.binary.Base64; +import org.linguafranca.pwdb.kdbx.simple.model.EntryClasses; +import org.linguafranca.pwdb.security.StreamEncryptor; +import org.simpleframework.xml.convert.Converter; +import org.simpleframework.xml.stream.InputNode; +import org.simpleframework.xml.stream.OutputNode; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +/** + * Value converter takes care of encryption and decryption of protected fields + */ +public class ValueConverter implements Converter { + + private final StreamEncryptor encryption; + + public ValueConverter(StreamEncryptor encryption) { + this.encryption = encryption; + } + + /** + * De-serialise (unmarshal) the input node + * @param node this is the node to deserialize the object from + * + * @return the de-serialised object + */ + @Override + public EntryClasses.StringProperty.Value read(InputNode node) throws Exception { + EntryClasses.StringProperty.Value value = new EntryClasses.StringProperty.Value(); + if (Objects.nonNull(node.getAttribute("Protected")) && node.getAttribute("Protected").getValue().equals("True")) { + byte[] encrypted = new byte[0]; + // you can only read the value once + String text = node.getValue(); + if (Objects.nonNull(text)) { + encrypted = Base64.decodeBase64(text.getBytes()); + } + String decrypted = new String(encryption.decrypt(encrypted), StandardCharsets.UTF_8); + value.setText(decrypted); + value.setProtected(null); + value.setProtectOnOutput(true); + } else { + value.setProtectOnOutput(false); + value.setText(node.getValue()); + } + return value; + } + + /** + * Serialise (marshal) the object + * @param node this is the node to serialized the object to + * @param value this is the value that is to be serialized + */ + @Override + public void write(OutputNode node, EntryClasses.StringProperty.Value value) throws Exception { + if (value.getProtectOnOutput()) { + byte [] encrypted = encryption.encrypt(value.getText().getBytes(StandardCharsets.UTF_8)); + byte [] base64Encoded = Base64.encodeBase64(encrypted); + node.setValue(new String(base64Encoded)); + node.getAttributes().put("Protected", "True"); + } else { + node.setValue(value.getText()); + } + //node.getAttributes().remove("protectInMemory"); + } +} diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/EntryClasses.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/EntryClasses.java index 9063437b..02285fe5 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/EntryClasses.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/EntryClasses.java @@ -124,11 +124,8 @@ public Value(String text, Boolean _protected) { @Attribute(name = "ProtectInMemory", required = false) @Convert(KeePassBooleanConverter.class) - // NB converters don't work on attributes -see KdbxOutputTransformer protected Boolean protectInMemory; @Attribute(name = "Protected", required = false) - @Convert(KeePassBooleanConverter.class) - // NB converters don't work on attributes -see KdbxOutputTransformer Boolean _protected; @Attribute(name = "kpj2-ProtectOnOutput", required = false) @Convert(KeePassBooleanConverter.class) @@ -136,6 +133,14 @@ public Value(String text, Boolean _protected) { @Text String text; + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + public void setProtectOnOutput(boolean aProtected) { this.protectOnOutput = aProtected; } @@ -143,6 +148,15 @@ public void setProtectOnOutput(boolean aProtected) { public boolean getProtectOnOutput() { return Objects.nonNull(this.protectOnOutput) && this.protectOnOutput; } + + + public Boolean getProtected() { + return _protected; + } + + public void setProtected(Boolean _protected) { + this._protected = _protected; + } } } diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxInputTransformer.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxInputTransformer.java deleted file mode 100644 index d46982d5..00000000 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxInputTransformer.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.pwdb.kdbx.simple.transformer; - -import org.linguafranca.pwdb.kdbx.Helpers; -import org.linguafranca.pwdb.security.StreamEncryptor; -import org.linguafranca.xml.XmlEventTransformer; - -import javax.xml.stream.XMLEventFactory; -import javax.xml.stream.events.Attribute; -import javax.xml.stream.events.StartElement; -import javax.xml.stream.events.XMLEvent; - -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -import static javax.xml.stream.XMLStreamConstants.CHARACTERS; -import static javax.xml.stream.XMLStreamConstants.END_ELEMENT; -import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; - -/** - * Transform protected elements on input - * - * @author jo - */ -@SuppressWarnings("WeakerAccess") -public class KdbxInputTransformer implements XmlEventTransformer { - private XMLEventFactory xmlEventFactory = new com.fasterxml.aalto.stax.EventFactoryImpl(); - private final StreamEncryptor streamEncryptor; - private boolean decryptContent; - - public KdbxInputTransformer (StreamEncryptor streamEncryptor) { - this.streamEncryptor = streamEncryptor; - } - XMLEventFactory eventFactory = com.fasterxml.aalto.stax.EventFactoryImpl.newInstance(); - - public XMLEvent transform (XMLEvent event) { - switch (event.getEventType()) { - case START_ELEMENT: { - StartElement startElement = event.asStartElement(); - Iterable attributeIterable = startElement::getAttributes; - List attributes = StreamSupport - .stream(attributeIterable.spliterator(), false) - .collect(Collectors.toList()); - - // find any element that is marked for protection - Attribute attribute = attributes - .stream() - .filter(a -> a.getName().getLocalPart().equalsIgnoreCase("Protected")) - .findFirst() - .orElse(null); - - // set flag so it gets encrypted and remove attribute, set attribute for output - if (attribute != null) { - if (attribute.getValue().equalsIgnoreCase("true")) { - decryptContent = true; - attributes.add(eventFactory.createAttribute("kpj2-ProtectOnOutput", "True")); - } - attributes.remove(attribute); - event = eventFactory.createStartElement( - event.asStartElement().getName(), - attributes.iterator(), - null); - } - break; - } - case END_ELEMENT: { - decryptContent = false; - break; - } - case CHARACTERS: { - if (decryptContent) { - String text = event.asCharacters().getData(); - text = new String(streamEncryptor.decrypt(Helpers.decodeBase64Content(text.getBytes(), false))); - event = xmlEventFactory.createCharacters(text); - } - break; - } - } - return event; - } -} diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxOutputTransformer.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxOutputTransformer.java deleted file mode 100644 index 42d90da8..00000000 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/transformer/KdbxOutputTransformer.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.pwdb.kdbx.simple.transformer; - -import org.linguafranca.pwdb.kdbx.Helpers; -import org.linguafranca.pwdb.security.StreamEncryptor; -import org.linguafranca.xml.XmlEventTransformer; - -import javax.xml.namespace.QName; -import javax.xml.stream.XMLEventFactory; -import javax.xml.stream.events.Attribute; -import javax.xml.stream.events.StartElement; -import javax.xml.stream.events.XMLEvent; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -import static javax.xml.stream.XMLStreamConstants.*; - -/** - * Transform protected elements on output - * - * @author jo - */ -@SuppressWarnings("WeakerAccess") -public class KdbxOutputTransformer implements XmlEventTransformer { - - private XMLEventFactory eventFactory = com.fasterxml.aalto.stax.EventFactoryImpl.newInstance(); - private StreamEncryptor encryptor; - private Boolean encryptContent = false; - - public KdbxOutputTransformer(StreamEncryptor encryptor) { - this.encryptor = encryptor; - } - - @Override - public XMLEvent transform(XMLEvent event) { - switch (event.getEventType()) { - case START_ELEMENT: { - StartElement startElement = event.asStartElement(); - Iterable attributeIterable = startElement::getAttributes; - // filter out the annoying "class" attribute that simple adds to "History" element - // also filter out Protected. - List attributes = StreamSupport - .stream(attributeIterable.spliterator(), false) - .filter(a -> { - String s = a.getName().getLocalPart(); - return(!s.equalsIgnoreCase("class") && !s.equalsIgnoreCase("Protected")); - }) - .collect(Collectors.toList()); - // find any element that is marked for protection - Attribute attribute = attributes - .stream() - .filter(a -> a.getName().getLocalPart().equalsIgnoreCase("kpj2-protectOnOutput")) - .findFirst() - .orElse(null); - // protect it - if (attribute != null) { - if (attribute.getValue().equalsIgnoreCase("true")) { - encryptContent = true; - attributes.add(eventFactory.createAttribute("Protected", "True")); - } - attributes.remove(attribute); - } - event = eventFactory.createStartElement( - event.asStartElement().getName(), - attributes.iterator(), - null); - - break; - } - case CHARACTERS: { - if (encryptContent) { - String unencrypted = event.asCharacters().getData(); - String encrypted = Helpers.encodeBase64Content(encryptor.encrypt(unencrypted.getBytes()), false); - event = eventFactory.createCharacters(encrypted); - } else { - // we want tabs not spaces for indentation - if (event.asCharacters().getData().startsWith("\n")) { - String output = event.asCharacters().getData().replaceAll(" ", "\t"); - event = eventFactory.createCharacters(output); - } - } - break; - } - case END_ELEMENT: { - encryptContent = false; - break; - } - } - return event; - } -} - diff --git a/simple/src/main/java/org/linguafranca/xml/XmlEventTransformer.java b/simple/src/main/java/org/linguafranca/xml/XmlEventTransformer.java deleted file mode 100644 index 787c00bd..00000000 --- a/simple/src/main/java/org/linguafranca/xml/XmlEventTransformer.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.xml; - -import javax.xml.stream.events.XMLEvent; - -/** - * An interface for allowing XML events to be transformed by {@link XmlOutputStreamFilter} - * and {@link XmlInputStreamFilter}. - * - * @author jo - */ -public interface XmlEventTransformer { - XMLEvent transform(XMLEvent event); - - @SuppressWarnings("unused") - class None implements XmlEventTransformer { - @Override - public XMLEvent transform(XMLEvent event) { - return event; - } - } -} \ No newline at end of file diff --git a/simple/src/main/java/org/linguafranca/xml/XmlInputStreamFilter.java b/simple/src/main/java/org/linguafranca/xml/XmlInputStreamFilter.java deleted file mode 100644 index 1fffe7f1..00000000 --- a/simple/src/main/java/org/linguafranca/xml/XmlInputStreamFilter.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.xml; - -import org.jetbrains.annotations.NotNull; - -import javax.xml.stream.*; -import javax.xml.stream.events.XMLEvent; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; - - -/** - * A input stream filter to accept a stream, interpret as XML, allow transformation - * as XML then forward as a stream. - * - *

        Although this means that the interpretation of the XML will happen - * twice, here and in the target application, some such applications - * do not accept XML streams. e.g. the Simple XML framework. - * - * @author jo - */ -public class XmlInputStreamFilter extends InputStream { - - private final XMLEventReader xmlEventReader; - private XMLEventWriter xmlEventWriter; - - private byte[] buffer = new byte[0]; - private volatile ByteArrayInputStream xmlInStream = new ByteArrayInputStream(buffer); - private volatile ByteArrayOutputStream xmlWriteStream = new ByteArrayOutputStream(); - private boolean done = false; - - - private InputStream inputStream; // the underlying input stream to read from - private XmlEventTransformer eventTransformer; - - public XmlInputStreamFilter(InputStream is, XmlEventTransformer transformer) throws XMLStreamException { - this.inputStream = is; - this.eventTransformer = transformer; - - XMLInputFactory inputFactory = new com.fasterxml.aalto.stax.InputFactoryImpl(); - this.xmlEventReader = inputFactory.createXMLEventReader(is); - - XMLOutputFactory outputFactory = new com.fasterxml.aalto.stax.OutputFactoryImpl(); - this.xmlEventWriter = outputFactory.createXMLEventWriter(xmlWriteStream); - } - - /** - * Gets bytes from the internal buffer and replenishes the buffer as necessary - * - * @param b a byte array to fill - * @param offset the offset to start from - * @param length the number of bytes to return - * @return the number of bytes actually returned, , -1 if end of file - * @throws IOException on error - */ - private int get(byte[] b, int offset, int length) throws IOException { - if (done) { - return -1; - } - int totalBytesRead = 0; - int bytesRead; - // read bytes from the transformed inputstream - while ((bytesRead = xmlInStream.read(b, offset, length)) < length && !done) { - if (bytesRead == -1) { - try { - loadEvents(); - } catch (XMLStreamException e) { - throw new IOException(e); - } - } else { - offset += bytesRead; - length -= bytesRead; - totalBytesRead += bytesRead; - } - } - return bytesRead > 0 ? totalBytesRead + bytesRead : totalBytesRead; - } - - - /** - * replenish the internal buffer - * @throws XMLStreamException if there was a problem - */ - private void loadEvents() throws XMLStreamException { - if (!xmlEventReader.hasNext()) { - done = true; - return; - } - // get the next xml input event - XMLEvent event = xmlEventReader.nextEvent(); - // transform it - event = eventTransformer.transform(event); - // reset ouput buffer - xmlWriteStream.reset(); - // write event to buffer - xmlEventWriter.add(event); - // flush the written bytes - xmlEventWriter.flush(); - // create an input stream from the bytes created - xmlInStream = new ByteArrayInputStream(xmlWriteStream.toByteArray()); - } - - @Override - public int read() throws IOException { - byte[] buffer = new byte[1]; - if (get(buffer, 0, 1) != 1) { - return -1; - } - return buffer[0] & 0xFF; - } - - @Override - public int read(@NotNull byte[] b, int offset, int length) throws IOException { - return get(b, offset, length); - } - - @Override - public void close() throws IOException { - inputStream.close(); - } - -} diff --git a/simple/src/main/java/org/linguafranca/xml/XmlOutputStreamFilter.java b/simple/src/main/java/org/linguafranca/xml/XmlOutputStreamFilter.java deleted file mode 100644 index 20431365..00000000 --- a/simple/src/main/java/org/linguafranca/xml/XmlOutputStreamFilter.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.xml; - -import javax.xml.stream.XMLEventReader; -import javax.xml.stream.XMLEventWriter; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.events.XMLEvent; -import java.io.IOException; -import java.io.OutputStream; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; - -/** - * A filter to accept a stream, interpret as XML, allow transformation - * as XML then forward as a stream. - * - *

        Although this means that the interpretation of the XML will happen - * twice, here and in the target application, some such applications - * do not accept XML streams. e.g. the Simple XML framework. - * - * @author jo - */ -@SuppressWarnings({"WeakerAccess", "unused"}) -public class XmlOutputStreamFilter extends PipedOutputStream { - - protected Future future; - protected PipedInputStream pipedInputStream; - - public XmlOutputStreamFilter(final OutputStream outputStream, final XmlEventTransformer eventTransformer) throws IOException { - - super(); - pipedInputStream = new PipedInputStream(this); - - Callable output = new Callable() { - public Boolean call() { - try { - XMLEventReader eventReader = new com.fasterxml.aalto.stax.InputFactoryImpl() - .createXMLEventReader(pipedInputStream); - XMLEventWriter eventWriter = new com.fasterxml.aalto.stax.OutputFactoryImpl() - .createXMLEventWriter(outputStream); - - XMLEvent event = null; - while (eventReader.hasNext()) { - event = eventReader.nextEvent(); - event = eventTransformer.transform(event); - eventWriter.add(event); - eventWriter.flush(); - } - - eventReader.close(); - eventWriter.flush(); - eventWriter.close(); - outputStream.flush(); - outputStream.close(); - } catch (XMLStreamException | IOException e) { - throw new IllegalStateException(e); - } - return true; - } - }; - future = Executors.newSingleThreadExecutor().submit(output); - } - - public void cancel(boolean interrupt){ - future.cancel(interrupt); - } - - public boolean isDone(){ - return future.isDone(); - } - - public boolean await() throws IOException { - try { - return future.get(); - } catch (InterruptedException | ExecutionException e) { - throw new IOException(e); - } - } - - @Override - public void flush() throws IOException { - super.flush(); - } - - @Override - public void close() throws IOException { - super.close(); - } -} - diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java deleted file mode 100644 index fa567fd0..00000000 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.pwdb.kdbx; - -import org.junit.Test; -import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxInputTransformer; -import org.linguafranca.pwdb.security.StreamEncryptor; -import org.linguafranca.xml.XmlInputStreamFilter; - -import javax.xml.stream.XMLStreamException; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintStream; -import java.util.Scanner; - -import static org.linguafranca.util.TestUtil.getTestPrintStream; - -/** - * @author jo - */ -public class KdbxInnerInputStreamTest { - static PrintStream printStream = getTestPrintStream(); - - // TODO what does this test, exactly? - @Test - public void test() throws XMLStreamException, IOException { - InputStream is = getClass().getClassLoader().getResourceAsStream("ExampleDatabase.xml"); - XmlInputStreamFilter sxd = new XmlInputStreamFilter(is, new KdbxInputTransformer(new StreamEncryptor.None())); - Scanner s = new Scanner(is); - while (s.hasNext()) { - printStream.println(s.nextLine()); - } - s.close(); - } - -} \ No newline at end of file diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerOutputStreamTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerOutputStreamTest.java deleted file mode 100644 index 8f6a3856..00000000 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerOutputStreamTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.pwdb.kdbx; - -import com.google.common.io.ByteStreams; -import org.junit.Ignore; -import org.junit.Test; -import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; -import org.linguafranca.pwdb.kdbx.simple.SimpleEntry; -import org.linguafranca.pwdb.kdbx.simple.transformer.KdbxOutputTransformer; -import org.linguafranca.xml.XmlEventTransformer; -import org.linguafranca.xml.XmlInputStreamFilter; -import org.linguafranca.xml.XmlOutputStreamFilter; - -import javax.xml.stream.XMLStreamException; -import java.io.*; - -import static org.junit.Assert.assertEquals; - -/** - * @author jo - */ -public class KdbxInnerOutputStreamTest { - - @Test - public void test() throws Exception { - final SimpleDatabase database = new SimpleDatabase(); - final SimpleEntry entry = database.newEntry(); - entry.setTitle("Password Encyption Test"); - entry.setPassword("password"); - database.getRootGroup().addEntry(entry); - database.save(new KdbxCreds.None(), ByteStreams.nullOutputStream()); - } - - @Test @Ignore - public void testOutputStreamFilter () throws IOException, XMLStreamException { - File temp = File.createTempFile("temp", "temp"); - OutputStream outputStream = new FileOutputStream(temp); - XmlOutputStreamFilter filter = new XmlOutputStreamFilter(outputStream, new KdbxOutputTransformer.None()); - outputStream.write("hello world".getBytes()); - outputStream.flush(); - outputStream.close(); - - InputStream inputStream = new FileInputStream(temp); - XmlInputStreamFilter filter1 = new XmlInputStreamFilter(inputStream, new XmlEventTransformer.None()); - byte[] b = new byte[1024]; - int l = filter1.read(b); - String s = new String(b,0,l); - assertEquals("hello world", s); - } - - -} \ No newline at end of file diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java index 73dce159..82c4733f 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java @@ -77,7 +77,7 @@ public void saveAndReloadTest() throws IOException { FileInputStream fis = new FileInputStream("testOutput/test1.kdbx"); D input = loadDatabase(getCreds("123".getBytes()), fis); verifyContents(input); - //saveDatabase(input, new StreamFormat.None(), new Credentials.None(), printStream); + //input.save(new StreamFormat.None(), new Credentials.None(), printStream); printStream.format("Test took %d millis", System.currentTimeMillis() - now); } From e37937c05a1537615e66dfae4e649b371a371736 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Thu, 18 May 2023 15:29:43 +0100 Subject: [PATCH 121/228] tidy Jaxb serialization --- .../kdbx/jaxb/JaxbSerializableDatabase.java | 120 ++++++++++-------- 1 file changed, 68 insertions(+), 52 deletions(-) diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java index 51cfed3f..b9a0f034 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java @@ -18,6 +18,7 @@ import com.sun.xml.txw2.output.IndentingXMLStreamWriter; import org.apache.commons.codec.binary.Base64; +import org.jetbrains.annotations.NotNull; import org.linguafranca.pwdb.SerializableDatabase; import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.jaxb.base.ValueBinding; @@ -103,62 +104,11 @@ public void afterUnmarshal(Object target, Object parent) { @Override public void save(OutputStream outputStream) { - final List toEncrypt = new ArrayList<>(); - if (keePassFile.getMeta().getMemoryProtection().getProtectTitle()) { - toEncrypt.add(org.linguafranca.pwdb.Entry.STANDARD_PROPERTY_NAME_TITLE); - } - if (keePassFile.getMeta().getMemoryProtection().getProtectURL()) { - toEncrypt.add(org.linguafranca.pwdb.Entry.STANDARD_PROPERTY_NAME_URL); - } - if (keePassFile.getMeta().getMemoryProtection().getProtectUserName()) { - toEncrypt.add(org.linguafranca.pwdb.Entry.STANDARD_PROPERTY_NAME_USER_NAME); - } - if (keePassFile.getMeta().getMemoryProtection().getProtectPassword()) { - toEncrypt.add(org.linguafranca.pwdb.Entry.STANDARD_PROPERTY_NAME_PASSWORD); - } - if (keePassFile.getMeta().getMemoryProtection().getProtectNotes()) { - toEncrypt.add(org.linguafranca.pwdb.Entry.STANDARD_PROPERTY_NAME_NOTES); - } try { JAXBContext jc = JAXBContext.newInstance(KeePassFile.class); Marshaller marshaller = jc.createMarshaller(); - marshaller.setListener(new Marshaller.Listener() { - String savedValue=""; - - // this changes the content on save, so we need to change it back again, see comment below - @Override - public void beforeMarshal(Object source) { - if (source instanceof StringField) { - StringField field = (StringField) source; - if (toEncrypt.contains(field.getKey()) || field.getValue().protectOnOutput) { - savedValue = field.getValue().getValue(); - byte [] encrypted = encryption.encrypt(field.getValue().getValue().getBytes(StandardCharsets.UTF_8)); - byte [] base64Encoded = Base64.encodeBase64(encrypted); - field.getValue().setValue(new String(base64Encoded)); - field.getValue().setProtected(true); - } else { - field.getValue().setProtected(false); - } - field.getValue().setProtectInMemory(false); - } - } - - // turns out that undoing the content change we made in beforeMarshal is the easiest way of doing this - // after a couple of days of looking at it. Making a clone before serialization - // is not practical and creating an adapter is not practical either, believe me, I tried. - // That said, if you are a JAXB whizz, and you know better ... - @Override - public void afterMarshal(Object source) { - if (source instanceof StringField) { - StringField field = (StringField) source; - if (field.getValue().getProtected()) { - field.getValue().setValue(savedValue); - field.getValue().setProtected(false); - } - } - } - }); + marshaller.setListener(createMarshallerListener(getToEncrypt())); XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newInstance(); XMLStreamWriter xmlStreamWriter = xmlOutputFactory.createXMLStreamWriter(outputStream); @@ -193,6 +143,72 @@ public void writeAttribute(String localName, String value) throws XMLStreamExcep } } + /** + * Creates a listener for marshalling that will rewrite fields that need encryption + */ + private Marshaller.Listener createMarshallerListener(List toEncrypt) { + return new Marshaller.Listener() { + String savedValue=""; + + // this changes the content on save, so we need to change it back again, see comment below + @Override + public void beforeMarshal(Object source) { + if (source instanceof StringField) { + StringField field = (StringField) source; + if (toEncrypt.contains(field.getKey()) || field.getValue().protectOnOutput) { + savedValue = field.getValue().getValue(); + byte [] encrypted = encryption.encrypt(field.getValue().getValue().getBytes(StandardCharsets.UTF_8)); + byte [] base64Encoded = Base64.encodeBase64(encrypted); + field.getValue().setValue(new String(base64Encoded)); + field.getValue().setProtected(true); + } else { + field.getValue().setProtected(false); + } + field.getValue().setProtectInMemory(false); + } + } + + // turns out that undoing the content change we made in beforeMarshal is the easiest way of doing this + // after a couple of days of looking at it. Making a clone before serialization + // is not practical and creating an adapter is not practical either, believe me, I tried. + // That said, if you are a JAXB whizz, and you know better ... + @Override + public void afterMarshal(Object source) { + if (source instanceof StringField) { + StringField field = (StringField) source; + if (field.getValue().getProtected()) { + field.getValue().setValue(savedValue); + field.getValue().setProtected(false); + } + } + } + }; + } + + /** + * Create a list of names of peroperties that should be encrypted by default + */ + @NotNull + private List getToEncrypt() { + final List toEncrypt = new ArrayList<>(); + if (keePassFile.getMeta().getMemoryProtection().getProtectTitle()) { + toEncrypt.add(org.linguafranca.pwdb.Entry.STANDARD_PROPERTY_NAME_TITLE); + } + if (keePassFile.getMeta().getMemoryProtection().getProtectURL()) { + toEncrypt.add(org.linguafranca.pwdb.Entry.STANDARD_PROPERTY_NAME_URL); + } + if (keePassFile.getMeta().getMemoryProtection().getProtectUserName()) { + toEncrypt.add(org.linguafranca.pwdb.Entry.STANDARD_PROPERTY_NAME_USER_NAME); + } + if (keePassFile.getMeta().getMemoryProtection().getProtectPassword()) { + toEncrypt.add(org.linguafranca.pwdb.Entry.STANDARD_PROPERTY_NAME_PASSWORD); + } + if (keePassFile.getMeta().getMemoryProtection().getProtectNotes()) { + toEncrypt.add(org.linguafranca.pwdb.Entry.STANDARD_PROPERTY_NAME_NOTES); + } + return toEncrypt; + } + @Override public StreamEncryptor getEncryption() { From 25e376dde6778ffcd9cc00c2334d4d33cd33a12f Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Thu, 18 May 2023 15:48:34 +0100 Subject: [PATCH 122/228] Update changelog and write KeePassJava2-DOM/JAXB/Simple as generator on save --- CHANGELOG.md | 4 ++++ .../org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java | 1 + .../java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java | 1 + .../org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java | 1 + .../org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java | 2 +- 5 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39054f4f..9eccb587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,16 +7,20 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.co ### Added - support for V4 files + - numerous updates to accommodate this ### Changed - Minimum version supported is Java 8 - updated documentation of various sorts - kdbx multithreaded fix +- fixes for a number of issues +- tidy up in various places - update dependencies - spongy castle replaced by bouncy castle - simplexml replaced by simple-xml-safe - update versions throughout + - list JAXB as external dependency for Java 11 - remove http module - don't deploy examples to Maven diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java index 3a0e5cba..d6ba2513 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapper.java @@ -118,6 +118,7 @@ public void save(Credentials credentials, public void save(StreamFormat streamFormat, Credentials credentials, OutputStream outputStream) throws IOException { + DomHelper.getElement("//Generator", domDatabase.getDoc().getDocumentElement(), false).setTextContent("KeePassJava2-DOM"); streamFormat.save(domDatabase, credentials, outputStream); setDirty(false); } diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java index 619e6a59..1059d52f 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java @@ -83,6 +83,7 @@ public void save(Credentials creds, OutputStream outputStream) throws IOExceptio } public void save(StreamFormat format, Credentials creds, OutputStream outputStream) throws IOException { + keePassFile.getMeta().setGenerator("KeePassJava2-JAXB"); JaxbSerializableDatabase jsd = new JaxbSerializableDatabase(this.keePassFile); format.save(jsd, creds, outputStream); setDirty(false); diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java index e1685e8f..7cafa52a 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java @@ -115,6 +115,7 @@ public void save(Credentials credentials, OutputStream outputStream) throws IOEx @Override public void save(StreamFormat streamFormat, Credentials credentials, OutputStream outputStream) throws IOException{ + keePassFile.meta.generator = "KeePassJava2-Simple"; SimpleSerializableDatabase simpleSerializableDatabase = new SimpleSerializableDatabase(this.keePassFile); streamFormat.save(simpleSerializableDatabase, credentials, outputStream); setDirty(false); diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java index 08cfb030..298f512c 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java @@ -64,7 +64,7 @@ public SimpleGroup getGroup() { @SuppressWarnings("unused") public static class Meta { @Element(name = "Generator") - protected String generator; + public String generator; @Element(name = "HeaderHash", required = false) @Convert(Base64ByteArrayConverter.class) public KeePassFile.ByteArray headerHash; From 494cf936056dbc3d3a3c35cfc0cd35bd204e2d3b Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Thu, 18 May 2023 17:25:13 +0100 Subject: [PATCH 123/228] Update POM and readme --- pom.xml | 2 +- readme.md | 34 ++++++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index ef9f2cd0..7d3ade12 100644 --- a/pom.xml +++ b/pom.xml @@ -75,7 +75,7 @@ 3.11.0 3.0.0 - 1.6.9 + 1.6.13 3.2.1 3.5.0 3.0.1 diff --git a/readme.md b/readme.md index 8c3b6ab5..56bf39ad 100644 --- a/readme.md +++ b/readme.md @@ -41,8 +41,7 @@ It is licensed under the Apache 2 License and is currently usable. After a a period of neglect, the project is (May 2023) back in development. -The current code is version 2.2-SNAPSHOT. This is on the main branch. It has -not yet been distributed to Maven. See [Build from Source](#build-from-source) +The current code is version 2.2-SNAPSHOT. This is on the main branch. See [Build from Source](#build-from-source) Key updates: - Java 8 (dependencies no longer support Java 7) @@ -53,18 +52,41 @@ See the [changelog](CHANGELOG.md) for more details. ## Maven Coordinates +### Release + The composite POM for the last release (2.1), Java 7 compatible, is org.linguafranca.pwdb KeePassJava2 2.1.4 - - at Maven Central. Note that the artifactId has become Camel Case from release 2.1.x onwards. + +at Maven Central. Note that the artifactId has become Camel Case from release 2.1.x onwards. + +### Snapshot + +Snapshot build at [Sonatype](https://oss.sonatype.org/content/repositories/snapshots/org/linguafranca/pwdb/): + + org.linguafranca.pwdb + KeePassJava2 + 2.2-SNAPSHOT + +with appropriate `` entry, like: + + + + oss.sonatype.org-snapshot + https://oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + There are also separate POMs for the various modules. The module structure is illustrated below under [Build from Source](#build-from-source). - - ## Java Version From e05b8c29782f51f6453257e1f9f7e7d319a2b7cb Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 22 May 2023 15:13:18 +0100 Subject: [PATCH 124/228] Sharpen docs, comments --- .../pwdb/kdbx/dom/DomSerializableDatabase.java | 2 -- jaxb/pom.xml | 8 +++++--- pom.xml | 5 +++++ readme.md | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java index 9c5ec683..9bfe4fce 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomSerializableDatabase.java @@ -85,7 +85,6 @@ public SerializableDatabase load(InputStream inputStream) throws IOException { doc = dBuilder.parse(inputStream); // we need to decrypt all protected fields - // TODO we assume they are all strings, which is wrong NodeList protectedContent = (NodeList) DomHelper.xpath.evaluate("//*[@Protected='True']", doc, XPathConstants.NODESET); for (int i = 0; i < protectedContent.getLength(); i++){ Element element = ((Element) protectedContent.item(i)); @@ -97,7 +96,6 @@ public SerializableDatabase load(InputStream inputStream) throws IOException { element.removeAttribute("Protected"); element.setAttribute("kpj2-ProtectOnOutput", "True"); } - return this; } catch (ParserConfigurationException e) { diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 5b311502..6f4ad86d 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -34,6 +34,7 @@ KeePassJava2-kdbx ${project.version} + javax.xml.bind jaxb-api @@ -71,10 +72,12 @@ + - generateSources - + generateJaxbSources + + @@ -90,7 +93,6 @@ - ../KDBX.4.xsd diff --git a/pom.xml b/pom.xml index 7d3ade12..f470ee61 100644 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,7 @@ false + ${inhibitConsoleOutput} @@ -100,6 +101,7 @@ ${maven.multiModuleProjectDirectory}/logback.xml + listener org.linguafranca.util.MavenRunListener @@ -189,6 +191,8 @@ slf4j-api 2.0.5 + ch.qos.logback logback-classic @@ -203,6 +207,7 @@ junit junit 4.13.2 + diff --git a/readme.md b/readme.md index 56bf39ad..e79bcfc8 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent) [![javadoc](https://javadoc.io/badge2/org.linguafranca.pwdb/KeePassJava2/javadoc.svg)](https://javadoc.io/doc/org.linguafranca.pwdb/KeePassJava2) -![alt text](https://badgen.net/badge/Build/2.2-SNAPSHOT/red?icon=github) +![alt text](https://badgen.net/badge/Build/2.2-SNAPSHOT/blue?icon=github) master [![CircleCI](https://dl.circleci.com/status-badge/img/gh/jorabin/KeePassJava2/tree/master.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/jorabin/KeePassJava2/tree/master) develop [![CircleCI](https://dl.circleci.com/status-badge/img/gh/jorabin/KeePassJava2/tree/develop.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/jorabin/KeePassJava2/tree/develop) From ee943af2a31499aa1b8b5e118d2a01fc04a24089 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 23 May 2023 10:56:33 +0100 Subject: [PATCH 125/228] Bump Bouncy Castle to library bckpix-jdk180n at release 1.73 #26 --- database/pom.xml | 3 +-- pom.xml | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/database/pom.xml b/database/pom.xml index f632e8ca..bbe7b516 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -23,8 +23,7 @@ org.bouncycastle - bcpkix-jdk15on + bcpkix-jdk18on - \ No newline at end of file diff --git a/pom.xml b/pom.xml index f470ee61..9581086e 100644 --- a/pom.xml +++ b/pom.xml @@ -183,8 +183,8 @@ org.bouncycastle - bcpkix-jdk15on - 1.70 + bcpkix-jdk18on + 1.73 org.slf4j From 59c6a602c174adafe48f80921ca46fff5d16767a Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 23 May 2023 11:36:39 +0100 Subject: [PATCH 126/228] Update diagrams --- DatabaseStructure.svg | 4 ++++ KdbxDiagram.svg | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 DatabaseStructure.svg diff --git a/DatabaseStructure.svg b/DatabaseStructure.svg new file mode 100644 index 00000000..20a5ade1 --- /dev/null +++ b/DatabaseStructure.svg @@ -0,0 +1,4 @@ + + + +
        Save/Load
        Save/Load
        SerializableDatabase
        (DOM, JAXB, Simple)
        SerializableDatabase...
        Encrypt/Decrypt
        Encrypt/Decrypt
        StreamFormat
        (Kdbx, None)
        StreamFormat...
        Read/Write Values
        Read/Write Values
        Save/Load
        Save/Load
        Database Implementation
        (Memory Model, DOM etc.)
        Database Implementation...
        Save/Load
        Save/Load
        User Read/Write
        User Read/Write
        Database Facade
        (DOM Wrapper, JAXB, SImple)
        Database Facade...
        Text is not SVG - cannot display
        \ No newline at end of file diff --git a/KdbxDiagram.svg b/KdbxDiagram.svg index 82baa74a..fb278b1e 100644 --- a/KdbxDiagram.svg +++ b/KdbxDiagram.svg @@ -1,2 +1,4 @@ + + -
        Magic Number
        Magic Number
        Version
        Version
        Header
        Header
        Header
        Header
        Header
        Header
        Header 0
        Header 0
        ...
        ...
        Value of Header 9
        Value of Header 9
        0
        0
        8
        8
        4
        4
        Type
        Type
        Length
        Length
        Value
        Value
        Encrypted
        Encrypted
        Unencrypted
        Unencrypted
        Sequence
        Sequence
        MD5 Hash
        MD5 Hash
        Length
        Length
        KDBX 3.1 File
        KDBX 3.1 File
        Header Format
        Header Format
        Hashed Block
        Format
        Hashed Block<div>Format</div>
        Unencrypted
        Payload
        (Optionally Compressed)
        [Not supported by viewer]
        36
        36
        0
        0
        40
        40
        12
        12
        1
        1
        3
        3
        0
        0
        Byte
        Offset
        [Not supported by viewer]
        Byte
        Offset
        [Not supported by viewer]
        Byte
        Offset
        [Not supported by viewer]
        Magic Number
        Magic Number
        Version
        Version
        Header
        Header
        Header
        Header
        Header
        Header
        Header 0
        Header 0
        ...
        ...
        0
        0
        8
        8
        32
        32
        Type
        Type
        Length
        Length
        Value
        Value
        Unencrypted
        Unencrypted
        Length
        Length
        Header Format
        Header Format
        Hashed Block
        Format
        Hashed Block<div>Format</div>
        HMAC-SHA-256
        HMAC-SHA-256
        36
        36
        0
        0
        12
        12
        1
        1
        5
        5
        0
        0
        Byte
        Offset
        [Not supported by viewer]
        Byte
        Offset
        [Not supported by viewer]
        Byte
        Offset
        [Not supported by viewer]
        KDBX 4 File
        KDBX 4 File
        http://keepass.info/help/kb/kdbx_4.html
        http://keepass.info/help/kb/kdbx_4.html
        SHA-256
        SHA-256
        Encrypted
        Payload
        (Optionally Compressed)
        [Not supported by viewer]
        HMAC-SHA-256
        HMAC-SHA-256
        Inner Header
        Inner Header
        Hashed Blocks
        Hashed Blocks
        Inner Header
        Inner Header
        ...
        ...
        Header 0
        Header 0
        XML Database
        XML Database
        Hashed Blocks
        Hashed Blocks
        XML Database
        XML Database
        \ No newline at end of file +
        Magic Number
        Magic Number
        Version
        Version
        Header
        Header
        Header
        Header
        Header
        Header
        Header 0
        Header 0
        ...
        ...
        Value of Header 9
        Value of Header 9
        0
        0
        8
        8
        4
        4
        Type
        Type
        Length
        Length
        Value
        Value
        Encrypted
        Encryp...
        Unencrypted
        Unencr...
        Sequence
        Sequence
        MD5 Hash
        MD5 Hash
        Length
        Length
        KDBX 3.1 File
        KDBX 3.1 File
        Header Format
        Header Format
        Hashed Block
        Format
        Hashed Block...
        Unencrypted
        Payload
        (Optionally Compressed)
        Unencrypted...
        36
        36
        0
        0
        40
        40
        12
        12
        1
        1
        3
        3
        0
        0
        Byte
        Offset
        Byte...
        Byte
        Offset
        Byte...
        Byte
        Offset
        Byte...
        Magic Number
        Magic Number
        Version
        Version
        Header
        Header
        Header
        Header
        Header
        Header
        Header 0
        Header 0
        ...
        ...
        0
        0
        8
        8
        32
        32
        Type
        Type
        Length
        Length
        Value
        Value
        Unencrypted
        Unencr...
        Length
        Length
        Header Format
        Header Format
        Hashed Block
        Format
        Hashed Block...
        HMAC-SHA-256
        HMAC-SHA-256
        36
        36
        0
        0
        12
        12
        1
        1
        5
        5
        0
        0
        Byte
        Offset
        Byte...
        Byte
        Offset
        Byte...
        Byte
        Offset
        Byte...
        KDBX 4 File
        KDBX 4 File
        As described at http://keepass.info/...
        SHA-256
        SHA-256
        Encrypted
        Payload
        (Optionally Compressed)
        Encrypted...
        HMAC-SHA-256
        HMAC-SHA-256
        Inner Header
        Inner Header
        Hashed Blocks
        Hashed Blocks
        Inner Header
        Inner Header
        ...
        ...
        Header 0
        Header 0
        XML Database
        XML Database
        Hashed Blocks
        Hashed Blocks
        XML Database
        XML Database
        Copyright (c) 2023 Jo Rabin, Licensed under the Apache2 license. See KeePassJava2.
        Copyright (c) 2023 Jo Rabin, Licensed under the Apache2 license. See KeePassJava2.
        Text is not SVG - cannot display
        \ No newline at end of file From 36fe1ee224b4e30ce1ead36ce317d960e8543bef Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 26 May 2023 13:14:13 +0100 Subject: [PATCH 127/228] Update Util to be able to do a hex dump from file chooser --- .../linguafranca/pwdb/kdbx/ChooseFile.java | 13 ++++++--- .../java/org/linguafranca/util/HexViewer.java | 29 ++++++++++--------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java index 28aa66f4..80cbf8ae 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java @@ -1,16 +1,15 @@ package org.linguafranca.pwdb.kdbx; import com.google.common.base.Strings; +import org.linguafranca.util.HexViewer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; import javax.swing.filechooser.FileFilter; -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintWriter; +import java.io.*; import java.net.URL; +import java.nio.file.Files; import java.nio.file.Paths; import static org.linguafranca.util.TestUtil.getTestPrintStream; @@ -41,6 +40,7 @@ public void choose() throws IOException { } catch (Exception e) { throw new RuntimeException(e); } + fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); fc.setFileFilter(new FileFilter() { @Override public boolean accept(File f) { @@ -67,9 +67,14 @@ public String getDescription() { return; } logger.info("Opening {}", fc.getSelectedFile().getPath()); +/* Util.listXml(fc.getSelectedFile().getName(), new KdbxCreds(s.getBytes()), new PrintWriter(outputStream)); +*/ + try (InputStream is = Files.newInputStream(Paths.get(fc.getSelectedFile().getPath()))) { + HexViewer.list(is); + } } } } diff --git a/test/src/main/java/org/linguafranca/util/HexViewer.java b/test/src/main/java/org/linguafranca/util/HexViewer.java index b562bed6..2ec49e0c 100644 --- a/test/src/main/java/org/linguafranca/util/HexViewer.java +++ b/test/src/main/java/org/linguafranca/util/HexViewer.java @@ -17,22 +17,23 @@ public static void main(String[] args) throws IOException { try (InputStream is = HexViewer.class.getClassLoader().getResourceAsStream("V4-AES-Argon2-CustomIcon.kdbx");) { + list(is); + } + } - //HexFormat format = HexFormat.ofDelimiter(" "); - - for (int i = 0; i < 32; i++) { - byte[] buf = new byte [16]; - is.read(buf); - StringBuilder sb = new StringBuilder(); - for (byte b: buf) { - sb.append(String.format("%02X ", b)); - } - sb.append(" "); - for (byte b : buf) { - sb.append(b < 0x20 || b > 0x7e ? (char) 0x00B7 : (char) b); - } - printStream.println(sb); + public static void list(InputStream is) throws IOException { + for (int i = 0; i < 32; i++) { + byte[] buf = new byte [16]; + is.read(buf); + StringBuilder sb = new StringBuilder(); + for (byte b: buf) { + sb.append(String.format("%02X ", b)); + } + sb.append(" "); + for (byte b : buf) { + sb.append(b < 0x20 || b > 0x7e ? (char) 0x00B7 : (char) b); } + printStream.println(sb); } } } From 12fc849df3b5f7356373cf6c61a2362fb21285e2 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 26 May 2023 13:16:34 +0100 Subject: [PATCH 128/228] KeePass2 needs variant dictionary to be encoded as unsigned values and KeePassXC is intolerant of V3 values in V4 headers #13 #39 --- .../org/linguafranca/pwdb/security/Aes.java | 2 +- .../linguafranca/pwdb/security/Argon2.java | 8 ++++---- .../pwdb/security/VariantDictionary.java | 15 ++++++++++++++ .../pwdb/kdbx/KdbxSerializer.java | 20 ++++++++++--------- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java index 5b6d3e3e..776cf39e 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java @@ -31,7 +31,7 @@ public class Aes implements CipherAlgorithm, KeyDerivationFunction { private static final VariantDictionary kdfParameters = new VariantDictionary((short) 1); static { kdfParameters.putUuid("$UUID", KDF); - kdfParameters.putLong(ParamRounds, 6000L); + kdfParameters.putULong(ParamRounds, 6000L); kdfParameters.putByteArray(ParamSeed, SecureRandom.getSeed(32)); } diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java b/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java index ca6533e6..a0ba2984 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java @@ -88,10 +88,10 @@ public byte[] getTransformedKey(byte[] digest, VariantDictionary argonParameterK @Override public VariantDictionary createKdfParameters() { VariantDictionary vd = new VariantDictionary((short) 1); - vd.putInt("P", 2); - vd.putInt("V", 19); - vd.putLong("I", 2); - vd.putLong("M", 64 * 1024 * 1024); + vd.putUInt("P", 2); + vd.putUInt("V", 19); + vd.putULong("I", 2); + vd.putULong("M", 64 * 1024 * 1024); vd.putUuid("$UUID", Argon2.argon2_kdf); vd.put("S", VariantDictionary.EntryType.ARRAY, random.generateSeed(32)); return vd; diff --git a/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java b/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java index c9bcc7fb..0a95db47 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java @@ -203,6 +203,15 @@ public void putLong(@NotNull String key, long value) { bb.putLong(value); entries.put(checkNotNull(key, knn), new Entry(INT64, buf)); } + /** + * Put a long as an unsigned64 under the key defined + */ + public void putULong(@NotNull String key, long value) { + byte[] buf = new byte[8]; + ByteBuffer bb = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN); + bb.putLong(value); + entries.put(checkNotNull(key, knn), new Entry(UINT64, buf)); + } public void putInt(@NotNull String key, int value) { byte[] buf = new byte[4]; @@ -210,4 +219,10 @@ public void putInt(@NotNull String key, int value) { bb.putInt(value); entries.put(checkNotNull(key, knn), new Entry(INT32, buf)); } + public void putUInt(@NotNull String key, int value) { + byte[] buf = new byte[4]; + ByteBuffer bb = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN); + bb.putInt(value); + entries.put(checkNotNull(key, knn), new Entry(UINT32, buf)); + } } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java index ab84988e..0e36f05a 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java @@ -432,17 +432,19 @@ public static void writeKdbxHeader(KdbxHeader kdbxHeader, OutputStream outputStr lengthWriter.accept(kdbxHeader.getEncryptionIv().length); ledos.write(kdbxHeader.getEncryptionIv()); - ledos.writeByte(HeaderType.INNER_RANDOM_STREAM_KEY); - lengthWriter.accept(kdbxHeader.getInnerRandomStreamKey().length); - ledos.write(kdbxHeader.getInnerRandomStreamKey()); + if (kdbxHeader.getVersion() < 4) { + ledos.writeByte(HeaderType.INNER_RANDOM_STREAM_KEY); + lengthWriter.accept(kdbxHeader.getInnerRandomStreamKey().length); + ledos.write(kdbxHeader.getInnerRandomStreamKey()); - ledos.writeByte(HeaderType.STREAM_START_BYTES); - lengthWriter.accept(kdbxHeader.getStreamStartBytes().length); - ledos.write(kdbxHeader.getStreamStartBytes()); + ledos.writeByte(HeaderType.STREAM_START_BYTES); + lengthWriter.accept(kdbxHeader.getStreamStartBytes().length); + ledos.write(kdbxHeader.getStreamStartBytes()); - ledos.writeByte(HeaderType.INNER_RANDOM_STREAM_ID); - lengthWriter.accept(4); - ledos.writeInt(kdbxHeader.getProtectedStreamAlgorithm().ordinal()); + ledos.writeByte(HeaderType.INNER_RANDOM_STREAM_ID); + lengthWriter.accept(4); + ledos.writeInt(kdbxHeader.getProtectedStreamAlgorithm().ordinal()); + } if (kdbxHeader.getVersion() > 3) { ledos.writeByte(HeaderType.KDF_PARAMETERS); From 664cf35c0029e018fd0fa094d88f0124bb95a8ea Mon Sep 17 00:00:00 2001 From: Giuseppe Valente Date: Fri, 18 Aug 2023 01:31:26 +0200 Subject: [PATCH 129/228] First Jackson integration --- .vscode/launch.json | 42 + .vscode/settings.json | 4 + jackson/pom.xml | 107 +++ .../linguafranca/pwdb/kdbx/jackson/App.java | 13 + .../pwdb/kdbx/jackson/JacksonDatabase.java | 206 +++++ .../pwdb/kdbx/jackson/JacksonEntry.java | 269 ++++++ .../pwdb/kdbx/jackson/JacksonGroup.java | 245 ++++++ .../pwdb/kdbx/jackson/JacksonHistory.java | 38 + .../pwdb/kdbx/jackson/JacksonIcon.java | 62 ++ .../jackson/JacksonSerializableDatabase.java | 103 +++ .../pwdb/kdbx/jackson/KeePassFile.java | 299 +++++++ .../converter/Base64ToByteConverter.java | 19 + .../converter/ByteToBase64Converter.java | 17 + .../kdbx/jackson/converter/TimeConverter.java | 18 + .../pwdb/kdbx/jackson/model/EntryClasses.java | 215 +++++ .../pwdb/kdbx/jackson/model/Times.java | 126 +++ .../pwdb/JacksonDatabaseLoadTest.java | 57 ++ .../pwdb/JacksonDatabaseTest.java | 20 + pom.xml | 803 ++++++++++++------ 19 files changed, 2384 insertions(+), 279 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 jackson/pom.xml create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/App.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonHistory.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonIcon.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/KeePassFile.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToByteConverter.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ByteToBase64Converter.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/TimeConverter.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/Times.java create mode 100644 jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoadTest.java create mode 100644 jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseTest.java diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..7d448b88 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,42 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "name": "Current File", + "request": "launch", + "mainClass": "${file}" + }, + { + "type": "java", + "name": "App", + "request": "launch", + "mainClass": "org.linguafranca.pwdb.kdbx.jackson.App", + "projectName": "KeePassJava2-jackson" + }, + { + "type": "java", + "name": "ChooseFile", + "request": "launch", + "mainClass": "org.linguafranca.pwdb.kdbx.ChooseFile", + "projectName": "example" + }, + { + "type": "java", + "name": "OpenDbExample", + "request": "launch", + "mainClass": "org.linguafranca.pwdb.kdbx.OpenDbExample", + "projectName": "example" + }, + { + "type": "java", + "name": "HexViewer", + "request": "launch", + "mainClass": "org.linguafranca.util.HexViewer", + "projectName": "test" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..c60d6d41 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "java.compile.nullAnalysis.mode": "automatic", + "maven.view": "hierarchical" +} \ No newline at end of file diff --git a/jackson/pom.xml b/jackson/pom.xml new file mode 100644 index 00000000..1215bf7e --- /dev/null +++ b/jackson/pom.xml @@ -0,0 +1,107 @@ + + + + + + KeePassJava2-parent + org.linguafranca.pwdb + 2.2-SNAPSHOT + ../pom.xml + + 4.0.0 + + KeePassJava2-jackson + KeePassJava2 :: Jackson + Contains a Jackson XML implementation for KDBX. + + + + org.linguafranca.pwdb + KeePassJava2-kdbx + ${project.version} + + + org.linguafranca.pwdb + test + ${project.version} + test + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + 2.15.0 + + + com.fasterxml.woodstox + woodstox-core + 6.5.0 + + + junit + junit + 4.11 + test + + + + + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + + diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/App.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/App.java new file mode 100644 index 00000000..76a27f0f --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/App.java @@ -0,0 +1,13 @@ +package org.linguafranca.pwdb.kdbx.jackson; + +/** + * Hello world! + * + */ +public class App +{ + public static void main( String[] args ) + { + System.out.println( "Hello World!" ); + } +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java new file mode 100644 index 00000000..bc092364 --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java @@ -0,0 +1,206 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx.jackson; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.StreamConfiguration; +import org.linguafranca.pwdb.StreamFormat; +import org.linguafranca.pwdb.base.AbstractDatabase; +import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; + +import com.fasterxml.jackson.core.exc.StreamReadException; +import com.fasterxml.jackson.databind.DatabindException; + +import static org.linguafranca.pwdb.kdbx.jackson.JacksonSerializableDatabase.createEmptyDatabase;; + +public class JacksonDatabase extends AbstractDatabase{ + + KeePassFile keePassFile; + StreamFormat streamFormat; + + public JacksonDatabase() throws StreamReadException, DatabindException, IOException { + this(createEmptyDatabase(), null); + } + + public JacksonDatabase(KeePassFile file, StreamFormat streamFormat) { + try { + keePassFile = file; + keePassFile.root.group.database = this; + this.streamFormat = streamFormat; + JacksonSerializableDatabase.fixUp(keePassFile.root.group); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + /** + * Load plaintext XML + * + * @param inputStream contains the XML + * @return a new Database + * @throws Exception on load failure + */ + public static JacksonDatabase loadXml(InputStream inputStream) throws Exception { + + KeePassFile keePassFile = new JacksonSerializableDatabase().load(inputStream).keePassFile; + keePassFile.root.group.uuid = UUID.randomUUID(); + return new JacksonDatabase(keePassFile, null); + } + + /** + * Load kdbx file + * + * @param credentials credentials to use + * @param inputStream where to load from + * @return a new database + */ + public static JacksonDatabase load(Credentials credentials, InputStream inputStream) throws IOException { + JacksonSerializableDatabase jsd = new JacksonSerializableDatabase(); + StreamFormat streamFormat = new KdbxStreamFormat(); + streamFormat.load(jsd, credentials, inputStream); + return new JacksonDatabase(jsd.keePassFile, streamFormat); + } + + /** + * Save the database with the same stream format that it was loaded with, or V4 default if none + * @param credentials credentials to use + * @param outputStream where to write to + */ + @Override + public void save(Credentials credentials, OutputStream outputStream) throws IOException { + // save with the stream format used to load if it exists, otherwise save V4 + // if (Objects.isNull(streamFormat)) { + // streamFormat = new KdbxStreamFormat(new KdbxHeader(4)); + // } + save(streamFormat, credentials, outputStream); + } + + /** + * Save the database with a choice of stream format + * @param streamFormat the format to use + * @param credentials credentials to use + * @param outputStream where to write to + */ + @Override + public void save(StreamFormat streamFormat, Credentials credentials, + OutputStream outputStream) throws IOException{ + // keePassFile.meta.generator = "KeePassJava2-Simple"; + // SimpleSerializableDatabase simpleSerializableDatabase = new SimpleSerializableDatabase(this.keePassFile); + // streamFormat.save(simpleSerializableDatabase, credentials, outputStream); + // setDirty(false); + } + + + @Override + public JacksonGroup getRootGroup() { + return keePassFile.root.group; + } + + @Override + public JacksonGroup newGroup() { + return JacksonGroup.createGroup(this); + } + + @Override + public JacksonEntry newEntry() { + return JacksonEntry.createEntry(this); + } + + @Override + public JacksonIcon newIcon() { + return new JacksonIcon(); + } + + @Override + public JacksonIcon newIcon(Integer integer) { + JacksonIcon ic = newIcon(); + ic.setIndex(integer); + return ic; + } + + @Override + public boolean isRecycleBinEnabled() { + return this.keePassFile.meta.recycleBinEnabled; + } + + @Override + public void enableRecycleBin(boolean enable) { + this.keePassFile.meta.recycleBinEnabled = enable; + } + + @Override + public JacksonGroup getRecycleBin() { + UUID recycleBinUuid = this.keePassFile.meta.recycleBinUUID; + JacksonGroup g = findGroup(recycleBinUuid); + if (g == null && isRecycleBinEnabled()) { + g = newGroup("Recycle Bin"); + getRootGroup().addGroup(g); + this.keePassFile.meta.recycleBinUUID = g.getUuid(); + this.keePassFile.meta.recycleBinChanged = new Date(); + } + return g; + } + + @Override + public String getName() { + return keePassFile.meta.databaseName; + } + + @Override + public void setName(String s) { + keePassFile.meta.databaseName = s; + keePassFile.meta.databaseNameChanged = new Date(); + setDirty(true); + } + + @Override + public String getDescription() { + return keePassFile.meta.databaseDescription; + } + + @Override + public void setDescription(String s) { + keePassFile.meta.databaseDescription = s; + keePassFile.meta.databaseDescriptionChanged = new Date(); + setDirty(true); + } + + @Override + public boolean shouldProtect(String s) { + return keePassFile.meta.memoryProtection.shouldProtect(s); + } + + + public List getBinaries() { + return keePassFile.getBinaries(); + } + + public void addBinary(byte [] bytes, int index) { + // SimpleSerializableDatabase.addBinary(this.keePassFile, index, bytes); + } + + public StreamFormat getStreamFormat() { + return streamFormat; + } + +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java new file mode 100644 index 00000000..0944c944 --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java @@ -0,0 +1,269 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx.jackson; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.linguafranca.pwdb.base.AbstractEntry; +import org.linguafranca.pwdb.kdbx.Helpers; + + +import static org.linguafranca.pwdb.kdbx.jackson.model.EntryClasses.*; +import org.linguafranca.pwdb.kdbx.jackson.model.Times; + + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "Entry") +public class JacksonEntry extends AbstractEntry { + + @JacksonXmlProperty(localName = "UUID") + protected UUID uuid; + @JacksonXmlProperty(localName = "IconID") + protected int iconID; + @JacksonXmlProperty(localName = "CustomIconUUID") + protected UUID customIconUUID; + @JacksonXmlProperty(localName = "ForegroundColor") + protected String foregroundColor; + @JacksonXmlProperty(localName = "BackgroundColor") + protected String backgroundColor; + @JacksonXmlProperty(localName = "OverrideURL") + protected String overrideURL; + @JacksonXmlProperty(localName = "Tags") + protected String tags; + @JacksonXmlProperty(localName = "Times") + protected Times times; + + @JacksonXmlProperty(localName = "String") /** Workaround jackson **/ + @JacksonXmlElementWrapper(useWrapping = false) + protected List string; + + @JacksonXmlProperty(localName = "Binary") /** Workaround jackson **/ + @JacksonXmlElementWrapper(useWrapping = false) + protected List binary; + + @JacksonXmlProperty(localName = "AutoType") + protected AutoType autoType; + + @JacksonXmlProperty(localName = "History") /** Workaround jackson **/ + @JacksonXmlElementWrapper(useWrapping = false) + protected List history; + + @JsonIgnore + JacksonDatabase database; + + @JsonIgnore + JacksonGroup parent; + + protected JacksonEntry() { + string = new ArrayList<>(); + binary = new ArrayList<>(); + times = new Times(); + uuid = UUID.randomUUID(); + iconID = 0; + } + + + public static JacksonEntry createEntry(JacksonDatabase database) { + JacksonEntry result = new JacksonEntry(); + result.database = database; + result.parent = null; + // avoiding setProperty as it does a touch(); + for (String p: STANDARD_PROPERTY_NAMES) { + result.string.add(new StringProperty(p, new StringProperty.Value(""))); + } + return result; + } + + @Override + public String getProperty(String s) { + return getStringContent(getStringProperty(s, string)); + } + + @Override + public void setProperty(String s, String s1) { + StringProperty sp; + if ((sp = getStringProperty(s, string)) != null) { + this.string.remove(sp); + } + this.string.add(new StringProperty(s, new StringProperty.Value(s1))); + touch(); + } + + @Override + public boolean removeProperty(String name) throws IllegalArgumentException { + if (STANDARD_PROPERTY_NAMES.contains(name)) throw new IllegalArgumentException("may not remove property: " + name); + + StringProperty sp = getStringProperty(name, string); + if (sp == null) { + return false; + } else { + this.string.remove(sp); + touch(); + return true; + } + } + + @Override + public List getPropertyNames() { + List result = new ArrayList<>(); + for (StringProperty property: this.string) { + result.add(property.getKey()); + } + return result; + } + + @Override + public byte[] getBinaryProperty(String s) { + BinaryProperty bp = getBinaryProp(s, binary); + if (bp == null) { + return null; + } + + KeePassFile.Binary binary = null; + for (KeePassFile.Binary b : database.getBinaries()) { + if (b.getId().equals(Integer.valueOf(getBinaryContent(bp)))) { + binary = b; + } + } + if (binary == null) { + return null; + } + return Helpers.decodeBase64Content(binary.getValue().getBytes(), binary.getCompressed()); + } + + @Override + public void setBinaryProperty(String s, byte[] bytes) { + // remove old binary property with same name + BinaryProperty bp = getBinaryProp(s, binary); + if (bp != null){ + binary.remove(bp); + } + + // what is the next free index in the binary store? + Integer max = -1; + for (KeePassFile.Binary binary: database.getBinaries()){ + if (binary.getId() > max) { + max = binary.getId(); + } + } + max++; + + database.addBinary(bytes, max); + + // make a reference to it from the entry + BinaryProperty binaryProperty = new BinaryProperty(); + binaryProperty.setKey(s); + BinaryProperty.Value fieldValue = new BinaryProperty.Value(); + fieldValue.setRef(String.valueOf(max)); + binaryProperty.setValue(fieldValue); + binary.add(binaryProperty); + touch(); + } + + @Override + public boolean removeBinaryProperty(String name) throws UnsupportedOperationException { + BinaryProperty bp = getBinaryProp(name, binary); + if (bp != null) { + binary.remove(bp); + touch(); + return true; + } + return false; + } + + @Override + public List getBinaryPropertyNames() { + List result = new ArrayList<>(); + for (BinaryProperty property: this.binary) { + result.add(property.getKey()); + } + return result; + } + + @Override + public JacksonGroup getParent() { + return parent; + } + + @Override + public @NotNull UUID getUuid() { + return uuid; + } + + @Override + public JacksonIcon getIcon() { + return new JacksonIcon(iconID); + } + + @Override + public void setIcon(JacksonIcon icon) { + iconID = icon.getIndex(); + } + + @Override + public Date getLastAccessTime() { + return times.getLastAccessTime(); + } + + @Override + public Date getCreationTime() { + return times.getCreationTime(); + } + + @Override + public boolean getExpires() { + return times.getExpires(); + } + + @Override + public void setExpires(boolean expires) { + times.setExpires(expires); + } + + @Override + public Date getExpiryTime() { + return times.getExpiryTime(); + } + + @Override + public void setExpiryTime(Date expiryTime) throws IllegalArgumentException { + if (expiryTime == null) throw new IllegalArgumentException("expiryTime may not be null"); + times.setExpiryTime(expiryTime); + } + + @Override + public Date getLastModificationTime() { + return times.getLastModificationTime(); + } + + @Override + protected void touch() { + this.times.setLastModificationTime(new Date()); + this.database.setDirty(true); + } + + +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java new file mode 100644 index 00000000..a8dfd115 --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java @@ -0,0 +1,245 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.linguafranca.pwdb.kdbx.jackson; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.jetbrains.annotations.NotNull; +import org.linguafranca.pwdb.kdbx.jackson.model.Times; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + + +@JacksonXmlRootElement(localName = "Group") +public class JacksonGroup extends org.linguafranca.pwdb.base.AbstractGroup { + + @JacksonXmlProperty(localName = "UUID") + protected UUID uuid; + + @JacksonXmlProperty(localName = "Name") + protected String name; + @JacksonXmlProperty(localName = "Notes") + protected String notes; + @JacksonXmlProperty(localName = "IconID") + protected int iconID; + @JacksonXmlProperty(localName = "CustomIconUUID") + protected UUID customIconUuid; + @JacksonXmlProperty(localName = "Times") + protected Times times; + @JacksonXmlProperty(localName = "IsExpanded") + protected Boolean isExpanded; + @JacksonXmlProperty(localName = "DefaultAutoTypeSequence") + protected String defaultAutoTypeSequence; + @JacksonXmlProperty(localName = "EnableAutoType") + protected Boolean enableAutoType; + @JacksonXmlProperty(localName = "EnableSearching") + protected Boolean enableSearching; + @JacksonXmlProperty(localName = "LastTopVisibleEntry") + protected UUID lastTopVisibleEntry; + + @JacksonXmlProperty(localName = "Entry") /** Workaround jackson **/ + @JacksonXmlElementWrapper(useWrapping = false) + protected List entry; + + @JacksonXmlProperty(localName = "Group") /** Workaround jackson **/ + @JacksonXmlElementWrapper(useWrapping = false) + protected List group; + + @JsonIgnore + protected JacksonDatabase database; + @JsonIgnore + protected JacksonGroup parent; + + protected JacksonGroup() { + entry = new ArrayList<>(); + group = new ArrayList<>(); + times = new Times(); + } + + public static JacksonGroup createGroup(JacksonDatabase database) { + JacksonGroup group = new JacksonGroup(); + group.database = database; + group.iconID = 0; + group.name = ""; + group.uuid = UUID.randomUUID(); + return group; + } + + @Override + public boolean isRootGroup() { + return database.getRootGroup().equals(this); + } + + @Override + public boolean isRecycleBin() { + return database.keePassFile.meta.recycleBinUUID.equals(this.uuid); + } + + @Override + public JacksonGroup getParent() { + return parent; + } + + @Override + public void setParent(JacksonGroup group) { + if (isRootGroup()) { + throw new IllegalStateException("Cannot add root group to another group"); + } + if (this.database != group.database) { + throw new IllegalStateException("Must be from same database"); + } + if (parent != null) { + parent.removeGroup(group); + parent.touch(); + } + parent = group; + parent.touch(); + touch(); + } + + @Override + public List getGroups() { + List result = new ArrayList<>(); + for (JacksonGroup aGroup : group) { + result.add(aGroup); + } + return result; + } + + @Override + public int getGroupsCount() { + return group.size(); + } + + @Override + public JacksonGroup addGroup(JacksonGroup group) { + if (group.isRootGroup()) { + throw new IllegalStateException("Cannot add root group to another group"); + } + if (this.database != group.database) { + throw new IllegalStateException("Must be from same database"); + } + if (group.getParent() != null) { + group.getParent().removeGroup(group); + } + group.parent = this; + this.group.add(group); + touch(); + return group; + } + + @Override + public JacksonGroup removeGroup(JacksonGroup group) { + if (this.database != group.database) { + throw new IllegalStateException("Must be from same database"); + } + this.group.remove(group); + group.parent = null; + touch(); + return group; + } + + @Override + public List getEntries() { + List result = new ArrayList<>(); + for (JacksonEntry entry: this.entry){ + result.add(entry); + } + return result; + } + + @Override + public int getEntriesCount() { + return this.entry.size(); + } + + @Override + public JacksonEntry addEntry(JacksonEntry entry) { + if (this.database != entry.database) { + throw new IllegalStateException("Must be from same database"); + } + if (entry.getParent() != null) { + entry.getParent().removeEntry(entry); + } + this.entry.add(entry); + entry.parent=this; + touch(); + return entry; + } + + @Override + public JacksonEntry removeEntry(JacksonEntry entry) { + if (this.database != entry.database) { + throw new IllegalStateException("Must be from same database"); + } + this.entry.remove(entry); + entry.parent = null; + return entry; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String s) { + this.name = s; + touch(); + } + + @Override + public UUID getUuid() { + return uuid; + } + + @Override + public JacksonIcon getIcon() { + return new JacksonIcon(iconID); + } + + @Override + public void setIcon(JacksonIcon icon) { + this.iconID = icon.getIndex(); + touch(); + } + + @NotNull + @Override + public JacksonDatabase getDatabase() { + return database; + } + + private void touch() { + if(this.times != null) { + this.times.setLastModificationTime(new Date()); + } + + if(this.database != null) { + this.database.setDirty(true); + } + + } +} + diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonHistory.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonHistory.java new file mode 100644 index 00000000..d3b7c9ea --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonHistory.java @@ -0,0 +1,38 @@ +package org.linguafranca.pwdb.kdbx.jackson; + +import java.util.List; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "History") +public class JacksonHistory { + + @JacksonXmlProperty(localName = "Entry") /** Workaround jackson **/ + @JacksonXmlElementWrapper(useWrapping = false) + private List entry; + + public JacksonHistory() { + + } + + /** + * @return the entry + */ + public List getEntry() { + return entry; + } + + /** + * @param entry the entry to set + */ + public void setEntry(List entry) { + this.entry = entry; + } + + + + + +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonIcon.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonIcon.java new file mode 100644 index 00000000..d6e220ee --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonIcon.java @@ -0,0 +1,62 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx.jackson; + +import org.linguafranca.pwdb.Icon; + +/** + * @author giusvale + */ +public class JacksonIcon implements Icon { + + private int index; + + public JacksonIcon(int iconID) { + index = iconID; + } + + public JacksonIcon() { + this.index = 0; + } + + @Override + public int getIndex() { + return this.index; + } + + @Override + public void setIndex(int index) { + this.index = index; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + JacksonIcon that = (JacksonIcon) o; + + return index == that.index; + + } + + @Override + public int hashCode() { + return index; + } + +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java new file mode 100644 index 00000000..967080df --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java @@ -0,0 +1,103 @@ +package org.linguafranca.pwdb.kdbx.jackson; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.linguafranca.pwdb.SerializableDatabase; +import org.linguafranca.pwdb.security.StreamEncryptor; + +import com.fasterxml.jackson.core.exc.StreamReadException; +import com.fasterxml.jackson.databind.DatabindException; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; + +public class JacksonSerializableDatabase implements SerializableDatabase{ + + public KeePassFile keePassFile; + private StreamEncryptor encryption; + + + public static KeePassFile createEmptyDatabase() throws StreamReadException, DatabindException, IOException { + + InputStream inputStream = JacksonSerializableDatabase.class.getClassLoader().getResourceAsStream("base.kdbx.xml"); + XmlMapper mapper = new XmlMapper(); + KeePassFile res = mapper.readValue(inputStream, KeePassFile.class); + return res; + + } + + public JacksonSerializableDatabase(){ + } + public JacksonSerializableDatabase(KeePassFile keePassFile) { + this.keePassFile = keePassFile; + } + + @Override + public JacksonSerializableDatabase load(InputStream inputStream) throws IOException { + XmlMapper mapper = new XmlMapper(); + keePassFile = mapper.readValue(inputStream, KeePassFile.class); + return this; + } + + @Override + public void save(OutputStream outputStream) throws IOException { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'save'"); + } + + @Override + public StreamEncryptor getEncryption() { + return this.encryption; + } + + @Override + public void setEncryption(StreamEncryptor encryption) { + this.encryption = encryption; + } + + @Override + public byte[] getHeaderHash() { + return keePassFile.meta.headerHash; } + + @Override + public void setHeaderHash(byte[] hash) { + this.keePassFile.meta.headerHash = hash; + } + + @Override + public void addBinary(int index, byte[] payload) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'addBinary'"); + } + + @Override + public byte[] getBinary(int index) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getBinary'"); + } + + @Override + public int getBinaryCount() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getBinaryCount'"); + } + + /** + * On load add parents + * @param parent a parent to recurse + */ + static void fixUp(JacksonGroup parent){ + + for (JacksonGroup group: parent.group) { + group.parent = parent; + group.database = parent.database; + fixUp(group); + } + + for (JacksonEntry entry: parent.entry) { + entry.database = parent.database; + entry.parent = parent; + } + } + +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/KeePassFile.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/KeePassFile.java new file mode 100644 index 00000000..c7512b4d --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/KeePassFile.java @@ -0,0 +1,299 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx.jackson; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.linguafranca.pwdb.Entry; +import org.linguafranca.pwdb.kdbx.jackson.converter.Base64ToByteConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.ByteToBase64Converter; +import org.linguafranca.pwdb.kdbx.jackson.converter.TimeConverter; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText; + +@JacksonXmlRootElement(localName = "KeePassFile") +public class KeePassFile { + + @JacksonXmlProperty(localName = "Meta") + public Meta meta; + @JacksonXmlProperty(localName = "Root") + public Root root; + + public List getBinaries() { + return meta.binaries; + } + + public void createBinaries() { + meta.binaries = new ArrayList<>(); + } + + public static class Root { + + public Root() {} + + @JacksonXmlProperty(localName = "Group") + public JacksonGroup group; + + @JacksonXmlProperty(localName = "DeletedObjects") /** Jackson workaround see https://github.com/FasterXML/jackson-dataformat-xml/issues/181#issuecomment-349411792 **/ + @JacksonXmlElementWrapper(useWrapping = false) + protected ArrayList deletedObjects; + + } + + public static class Meta { + + public Meta() {} + @JacksonXmlProperty(localName = "Generator") + public String generator; + + @JacksonXmlProperty(localName = "HeaderHash") + //public KeePassFile.ByteArray headerHash; + //public String headerHash; + @JsonDeserialize(converter = Base64ToByteConverter.class) + @JsonSerialize(converter = ByteToBase64Converter.class) + public byte[] headerHash; + + @JacksonXmlProperty(localName = "DatabaseName") + public String databaseName; + + @JacksonXmlProperty(localName = "DatabaseNameChanged") + @JsonDeserialize(converter = TimeConverter.class) + public Date databaseNameChanged; + + @JacksonXmlProperty(localName = "DatabaseDescription") + public String databaseDescription; + + @JacksonXmlProperty(localName = "DatabaseDescriptionChanged") + @JsonDeserialize(converter = TimeConverter.class) + public Date databaseDescriptionChanged; + + @JacksonXmlProperty(localName = "DefaultUserName") + protected String defaultUserName; + + @JacksonXmlProperty(localName = "DefaultUserNameChanged") + @JsonDeserialize(converter = TimeConverter.class) + protected Date defaultUserNameChanged; + + @JacksonXmlProperty(localName = "MaintenanceHistoryDays") + protected int maintenanceHistoryDays; + + @JacksonXmlProperty(localName = "Color") + protected String color; + + @JacksonXmlProperty(localName = "MasterKeyChanged") + @JsonDeserialize(converter = TimeConverter.class) + protected Date masterKeyChanged; + + @JacksonXmlProperty(localName = "MasterKeyChangeRec") + protected int masterKeyChangeRec; + + @JacksonXmlProperty(localName = "MasterKeyChangeForce") + protected int masterKeyChangeForce; + + @JacksonXmlProperty(localName = "MemoryProtection") + public KeePassFile.MemoryProtection memoryProtection; + + @JacksonXmlElementWrapper(localName = "CustomIcons") + protected ArrayList customIcons; + @JacksonXmlProperty(localName = "RecycleBinEnabled") + public Boolean recycleBinEnabled; + + @JacksonXmlProperty(localName = "RecycleBinUUID") + public UUID recycleBinUUID; + + @JacksonXmlProperty(localName = "RecycleBinChanged") + @JsonDeserialize(converter = TimeConverter.class) + public Date recycleBinChanged; + + @JacksonXmlProperty(localName = "EntryTemplatesGroup") + + protected UUID entryTemplatesGroup; + + @JacksonXmlProperty(localName = "EntryTemplatesGroupChanged") + @JsonDeserialize(converter = TimeConverter.class) + protected Date entryTemplatesGroupChanged; + + @JacksonXmlProperty(localName = "LastSelectedGroup") + + protected UUID lastSelectedGroup; + @JacksonXmlProperty(localName = "LastTopVisibleGroup") + + protected UUID lastTopVisibleGroup; + @JacksonXmlProperty(localName = "HistoryMaxItems") + protected int historyMaxItems; + @JacksonXmlProperty(localName = "HistoryMaxSize") + protected int historyMaxSize; + + @JacksonXmlElementWrapper(localName = "Binaries") + protected List binaries; + + @JacksonXmlProperty(localName = "CustomData") + protected KeePassFile.CustomData customData; + + /* version 4 */ + + @JacksonXmlProperty(localName = "SettingsChanged") + @JsonDeserialize(converter = TimeConverter.class) + protected Date settingsChanged; + } + + + public static class MemoryProtection { + public MemoryProtection() {} + + @JacksonXmlProperty(localName = "ProtectTitle") + + protected Boolean protectTitle; + @JacksonXmlProperty(localName = "ProtectUserName") + + protected Boolean protectUserName; + @JacksonXmlProperty(localName = "ProtectPassword") + + protected Boolean protectPassword; + @JacksonXmlProperty(localName = "ProtectURL") + + protected Boolean protectURL; + @JacksonXmlProperty(localName = "ProtectNotes") + + protected Boolean protectNotes; + + public Boolean shouldProtect(String name) { + switch(name) { + case Entry.STANDARD_PROPERTY_NAME_TITLE: return protectTitle; + case Entry.STANDARD_PROPERTY_NAME_USER_NAME: return protectUserName; + case Entry.STANDARD_PROPERTY_NAME_PASSWORD: return protectPassword; + case Entry.STANDARD_PROPERTY_NAME_URL: return protectURL; + case Entry.STANDARD_PROPERTY_NAME_NOTES: return protectNotes; + } + return false; + } + } + + public static class Binaries { + public Binaries() {} + } + + @JacksonXmlRootElement(localName = "Binary") + public static class Binary { + + public Binary() {} + + @JacksonXmlText + protected String value; + + @JacksonXmlProperty(localName = "ID", isAttribute = true) + protected Integer id; + + @JacksonXmlProperty(localName = "Compressed", isAttribute = true) + protected Boolean compressed; + + public String getName() { + return String.valueOf(id); + } + + public String getValue() { + return value; + } + + public Boolean getCompressed() { + return compressed; + } + + public Integer getId() { + return id; + } + + public void setId(Integer Id) { + this.id = Id; + } + + public void setValue(String value) { + this.value = value; + } + + public void setCompressed(boolean compressed) { + this.compressed = compressed; + } + } + + @JacksonXmlRootElement(localName = "Icon") + public static class Icon { + public Icon() {} + @JacksonXmlProperty(localName = "UUID") + protected UUID uuid; + @JacksonXmlProperty(localName = "Name") + protected String name; + @JacksonXmlProperty(localName = "LastModificationTime") + @JsonDeserialize(converter = TimeConverter.class) + protected Date lastModificationTime; + @JacksonXmlProperty(localName = "Data") + @JsonDeserialize(converter = Base64ToByteConverter.class) + @JsonSerialize(converter = ByteToBase64Converter.class) + protected byte[] data; + + public String getName() { + return uuid.toString(); + } + } + + /* public static class ByteArray { + + public ByteArray() { + + } + + private byte[] content; + + public ByteArray(byte[] content) { + this.content = content; + } + + public byte[] getContent() { + return content; + } + + public void setContent(byte[] content) { + this.content = content; + } + } */ + + public static class CustomData { + + public CustomData() {} + + protected List any; + } + + @JacksonXmlRootElement(localName = "DeletedObjects") + public static class DeletedObject { + public DeletedObject() {} + + @JacksonXmlProperty(localName = "UUID") + protected UUID uuid; + @JacksonXmlProperty(localName = "DeletionTime") + @JsonDeserialize(converter = TimeConverter.class) + protected Date deletionTime; + } +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToByteConverter.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToByteConverter.java new file mode 100644 index 00000000..355041a5 --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToByteConverter.java @@ -0,0 +1,19 @@ +package org.linguafranca.pwdb.kdbx.jackson.converter; + +import java.util.Base64; + +import com.fasterxml.jackson.databind.util.StdConverter; + +public class Base64ToByteConverter extends StdConverter { + + @Override + public byte[] convert(String value) { + byte[] decode = Base64.getDecoder().decode(value); + return decode; + } + + + + + +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ByteToBase64Converter.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ByteToBase64Converter.java new file mode 100644 index 00000000..6d344dc9 --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ByteToBase64Converter.java @@ -0,0 +1,17 @@ +package org.linguafranca.pwdb.kdbx.jackson.converter; + +import java.math.BigInteger; + +import com.fasterxml.jackson.databind.util.StdConverter; + + +public class ByteToBase64Converter extends StdConverter{ + + @Override + public String convert(byte[] value) { + String binaryStr = new BigInteger(1, value).toString(2); + return binaryStr; + + } + +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/TimeConverter.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/TimeConverter.java new file mode 100644 index 00000000..ba501603 --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/TimeConverter.java @@ -0,0 +1,18 @@ +package org.linguafranca.pwdb.kdbx.jackson.converter; + +import java.util.Date; + +import org.linguafranca.pwdb.kdbx.Helpers; + +import com.fasterxml.jackson.databind.util.StdConverter; + +public class TimeConverter extends StdConverter{ + + @Override + public Date convert(String value) { + if (value.equals("${creationDate}")) { + return new Date(); + } + return Helpers.toDate(value); + } +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java new file mode 100644 index 00000000..72e2d391 --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java @@ -0,0 +1,215 @@ +/* + * Copyright 2015 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx.jackson.model; + + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.linguafranca.pwdb.kdbx.jackson.JacksonEntry; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText; + + +public abstract class EntryClasses { + public static StringProperty getStringProperty(String name, List string) { + for (StringProperty property : string) { + if (property.key.equals(name)) { + return property; + } + } + return null; + } + + public static String getStringContent(StringProperty property) { + return property == null || property.value == null? null:property.value.text; + } + + public static BinaryProperty getBinaryProp(String name, List binary) { + for (BinaryProperty property : binary) { + if (property.key.equals(name)) { + return property; + } + } + return null; + } + + public static String getBinaryContent(BinaryProperty property) { + return property == null || property.value == null ? null : property.value.ref; + } + + @JacksonXmlRootElement(localName = "AutoType") + public static class AutoType { + @JacksonXmlProperty(localName = "Enabled") + protected Boolean enabled; + + @JacksonXmlProperty(localName = "DataTransferObfuscation") + protected int dataTransferObfuscation; + + @JacksonXmlProperty(localName = "DefaultSequence") + protected String defaultSequence; + + @JacksonXmlProperty(localName = "Association") + protected AutoType.Association association; + + public static class Association { + + @JacksonXmlProperty(localName = "Window") + protected String window; + @JacksonXmlProperty(localName = "KeystrokeSequence") + protected String keystrokeSequence; + + protected List windowAndKeystrokeSequence; + } + } + + @JacksonXmlRootElement(localName = "String") + public static class StringProperty { + + public StringProperty() { + this("",new Value()); + } + public StringProperty(String key, Value value) { + this.key = key; + this.value = value; + } + + @JacksonXmlProperty(localName = "Key") + String key; + + @JacksonXmlProperty(localName = "Value") + Value value; + + public String getName() { + return key; + } + + public String getKey() { + return key; + } + + public Value getValue() { + return value; + } + + @JacksonXmlRootElement(localName = "Value") + public static class Value { + public Value(){ + this(""); + } + public Value(String text) { + this.text = text; + this._protected = false; + } + + public Value(String text, Boolean _protected) { + this._protected = _protected; + this.text = text; + } + + @JacksonXmlProperty(localName = "ProtectInMemory", isAttribute = true) + protected Boolean protectInMemory; + + @JacksonXmlProperty(localName = "Protected", isAttribute = true) + Boolean _protected; + + @JacksonXmlProperty(localName = "kpj2-ProtectOnOutput", isAttribute = true) + Boolean protectOnOutput; + + @JacksonXmlText + String text; + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public void setProtectOnOutput(boolean aProtected) { + this.protectOnOutput = aProtected; + } + + public boolean getProtectOnOutput() { + return Objects.nonNull(this.protectOnOutput) && this.protectOnOutput; + } + + + public Boolean getProtected() { + return _protected; + } + + public void setProtected(Boolean _protected) { + this._protected = _protected; + } + } + } + + @JacksonXmlRootElement(localName = "Binary") + public static class BinaryProperty { + + @JacksonXmlProperty(localName = "Key") + String key; + + @JacksonXmlProperty(localName = "Value") + Value value; + + public String getKey() { + return key; + } + + public String getName() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public void setValue(Value value) { + this.value = value; + } + + @JacksonXmlRootElement(localName = "Value") + public static class Value { + @JacksonXmlProperty(localName = "Ref", isAttribute = true) + String ref; + + public void setRef(String ref) { + this.ref = ref; + } + } + } + + public static class History { + + @JacksonXmlElementWrapper(localName = "JacksonEntry", useWrapping = false) + private List list; + + public History(){ + list = new ArrayList<>(); + } + public List getHistory(){ + return list; + } + } +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/Times.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/Times.java new file mode 100644 index 00000000..cbe668b4 --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/Times.java @@ -0,0 +1,126 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx.jackson.model; + + +import java.util.Date; + +import org.linguafranca.pwdb.kdbx.jackson.converter.TimeConverter; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + + + +@JacksonXmlRootElement(localName = "Times") +public class Times { + @JacksonXmlProperty(localName = "LastModificationTime") + @JsonDeserialize(converter = TimeConverter.class) + protected Date lastModificationTime; + + @JacksonXmlProperty(localName = "CreationTime") + @JsonDeserialize(converter = TimeConverter.class) + protected Date creationTime; + + @JacksonXmlProperty(localName = "LastAccessTime") + @JsonDeserialize(converter = TimeConverter.class) + protected Date lastAccessTime; + + @JacksonXmlProperty(localName = "ExpiryTime") + @JsonDeserialize(converter = TimeConverter.class) + protected Date expiryTime; + + @JacksonXmlProperty(localName = "Expires") + protected Boolean expires; + + @JacksonXmlProperty(localName = "UsageCount") + protected int usageCount; + + @JacksonXmlProperty(localName = "LocationChanged") + @JsonDeserialize(converter = TimeConverter.class) + protected Date locationChanged; + + public Date getLastModificationTime() { + return lastModificationTime; + } + + public void setLastModificationTime(Date lastModificationTime) { + this.lastModificationTime = lastModificationTime; + } + + public Date getCreationTime() { + return creationTime; + } + + public void setCreationTime(Date creationTime) { + this.creationTime = creationTime; + } + + public Date getLastAccessTime() { + return lastAccessTime; + } + + public void setLastAccessTime(Date lastAccessTime) { + this.lastAccessTime = lastAccessTime; + } + + public Date getExpiryTime() { + return expiryTime; + } + + public void setExpiryTime(Date expiryTime) { + this.expiryTime = expiryTime; + } + + public Boolean getExpires() { + return expires; + } + + public void setExpires(Boolean expires) { + this.expires = expires; + } + + public int getUsageCount() { + return usageCount; + } + + public void setUsageCount(int usageCount) { + this.usageCount = usageCount; + } + + public Date getLocationChanged() { + return locationChanged; + } + + public void setLocationChanged(Date locationChanged) { + this.locationChanged = locationChanged; + } + + public Times() { + this(new Date(System.currentTimeMillis()/1000*1000)); + } + public Times(Date date) { + lastModificationTime = date; + lastAccessTime = date; + locationChanged = date; + creationTime = date; + expiryTime = date; + expires = false; + usageCount = 0; + } +} diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoadTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoadTest.java new file mode 100644 index 00000000..c71a67d7 --- /dev/null +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoadTest.java @@ -0,0 +1,57 @@ +package org.linguafranca.pwdb; + +import java.io.InputStream; +import java.io.PrintStream; + +import org.junit.Test; +import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; + +import static org.linguafranca.util.TestUtil.getTestPrintStream; + +public class JacksonDatabaseLoadTest { + + static PrintStream printStream = getTestPrintStream(); + + + @Test + public void loadXml() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("ExampleDatabase.xml"); + JacksonDatabase database = JacksonDatabase.loadXml(inputStream); + database.visit(new Visitor.Print(printStream)); + } + @Test + public void loadKdbx() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test123.kdbx"); + JacksonDatabase database = JacksonDatabase.load(new KdbxCreds("123".getBytes()), inputStream); + database.visit(new Visitor.Print(printStream)); + } + + @Test + public void loadKdbxV4() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); + JacksonDatabase database = JacksonDatabase.load(new KdbxCreds("123".getBytes()), inputStream); + database.visit(new Visitor.Print(printStream)); + // test what happens to dates in V4 + database.visit(new Visitor.Default(){ + @Override + public void visit(Entry entry) { + printStream.println(entry.getCreationTime()); + } + }); + } + + @Test + public void emptyDb() throws Exception { + JacksonDatabase database = new JacksonDatabase(); + printStream.println(database.getDescription()); + } + + @Test + public void dbWithDeleted() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("testDeleted.kdbx"); + JacksonDatabase database = JacksonDatabase.load(new KdbxCreds("123".getBytes()), inputStream); + database.visit(new Visitor.Print(printStream)); + } + +} diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseTest.java new file mode 100644 index 00000000..1f4dfdd5 --- /dev/null +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseTest.java @@ -0,0 +1,20 @@ +package org.linguafranca.pwdb; + +import java.io.IOException; + +import org.linguafranca.pwdb.checks.BasicDatabaseChecks; +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; + +public class JacksonDatabaseTest extends BasicDatabaseChecks { + + public JacksonDatabaseTest() throws IOException { + + } + + + @Override + public Database createDatabase() throws IOException { + return new JacksonDatabase(); + } + +} diff --git a/pom.xml b/pom.xml index 9581086e..496fb3aa 100644 --- a/pom.xml +++ b/pom.xml @@ -1,4 +1,4 @@ - + - - - 4.0.0 - - org.linguafranca.pwdb - KeePassJava2-parent - 2.2-SNAPSHOT - - database - test - kdb - kdbx - dom - simple - jaxb - all - example - - pom - - KeePassJava2 - A Java 8 API for databases compatible with the renowned KeePass password safe for Windows + --> + + 4.0.0 + + org.linguafranca.pwdb + + KeePassJava2-parent + + 2.2-SNAPSHOT + + + + database + + test + + kdb + + kdbx + + dom + + simple + + jaxb + + all + + example + + jackson + + + + pom + + KeePassJava2 + + A Java 8 API for databases compatible with the renowned KeePass password safe for Windows + + https://github.com/jorabin/KeePassJava2 + + + + + + Apache License, Version 2.0 + + http://www.apache.org/licenses/LICENSE-2.0.txt + + repo + + + + + + + + scm:git:git://github.com/jorabin/KeePassJava2.git + + scm:git:git@github.com:jorabin/KeePassJava2.git + https://github.com/jorabin/KeePassJava2 - - - - Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - repo - - - - - scm:git:git://github.com/jorabin/KeePassJava2.git - scm:git:git@github.com:jorabin/KeePassJava2.git - https://github.com/jorabin/KeePassJava2 - HEAD - - - - - Jo Rabin - Europe/London - https://www.linkedin.com/in/jorabin - - Creator - - - - - - 3.0.4 - - - - UTF-8 - 1.8 - - 3.11.0 - 3.0.0 - 1.6.13 - 3.2.1 - 3.5.0 - 3.0.1 - 3.0.0 - 3.0.2 - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - false - - - ${inhibitConsoleOutput} - - - - ${maven.multiModuleProjectDirectory}/logback.xml - - - - - listener - org.linguafranca.util.MavenRunListener - - - - - - org.apache.maven.plugins - maven-jar-plugin - ${maven-jar-plugin.version} - - - - ${project.groupId}.${project.artifactId} - - - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - ${maven-compiler-plugin.version} - - ${java.version} - ${java.version} - - - - org.apache.maven.plugins - maven-release-plugin - ${maven-release-plugin.version} - - true - false - releaseToMaven,signJar - deploy - - - - org.apache.maven.plugins - maven-dependency-plugin - 3.0.2 - - - - - - - junit - junit - - - org.slf4j - slf4j-api - - - ch.qos.logback - logback-classic - - - - - - - org.jetbrains - annotations - 24.0.1 - - - - com.google.guava - guava - 31.1-android - - - org.bouncycastle - bcpkix-jdk18on - 1.73 - - - org.slf4j - slf4j-api - 2.0.5 - - - - ch.qos.logback - logback-classic - 1.3.7 - - - commons-codec - commons-codec - 1.15 - - - junit - junit - 4.13.2 - - - - - - - - inhibitConsoleOutput + + HEAD + + + + + + + + Jo Rabin + + Europe/London + + https://www.linkedin.com/in/jorabin + + + + Creator + + + + + + + + + + 3.0.4 + + + + + + UTF-8 + + 1.8 + + 3.11.0 + + 3.0.0 + + 1.6.13 + + 3.2.1 + + 3.5.0 + + 3.0.1 + + 3.0.0 + + 3.0.2 + + + + + + + + + + + + org.apache.maven.plugins + + maven-surefire-plugin + + ${maven-surefire-plugin.version} + + + + false + + + + + + ${inhibitConsoleOutput} + + + + + + + + ${maven.multiModuleProjectDirectory}/logback.xml + + + - true + + + + + + listener + + org.linguafranca.util.MavenRunListener + + + - - - signJar - - - - org.apache.maven.plugins - maven-gpg-plugin - ${maven-gpg-plugin.version} - - - sign-artifacts - verify - - sign - - - - - - - - - releaseToMaven - - - - org.sonatype.plugins - nexus-staging-maven-plugin - ${nexus-staging-maven-plugin.version} - true - - ossrh - https://oss.sonatype.org/ - false - - - - org.apache.maven.plugins - maven-source-plugin - ${maven-source-plugin.version} - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - ${maven-javadoc-plugin.version} - - - attach-javadocs - - jar - - - - - - - - - - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - - \ No newline at end of file + + + + + + + + org.apache.maven.plugins + + maven-jar-plugin + + ${maven-jar-plugin.version} + + + + + + + + ${project.groupId}.${project.artifactId} + + + + + + + + + + + + + + + + + + org.apache.maven.plugins + + maven-compiler-plugin + + ${maven-compiler-plugin.version} + + + + ${java.version} + + ${java.version} + + + + + + + + org.apache.maven.plugins + + maven-release-plugin + + ${maven-release-plugin.version} + + + + true + + false + + releaseToMaven,signJar + + deploy + + + + + + + + org.apache.maven.plugins + + maven-dependency-plugin + + 3.0.2 + + + + + + + + + + + + junit + + junit + + + + + + org.slf4j + + slf4j-api + + + + + + ch.qos.logback + + logback-classic + + + + + + + + + + + + org.jetbrains + + annotations + + 24.0.1 + + + + + + + + com.google.guava + + guava + + 31.1-android + + + + + + org.bouncycastle + + bcpkix-jdk18on + + 1.73 + + + + + + org.slf4j + + slf4j-api + + 2.0.5 + + + + + + + + ch.qos.logback + + logback-classic + + 1.3.7 + + + + + + commons-codec + + commons-codec + + 1.15 + + + + + + junit + + junit + + 4.13.2 + + + + + + + + + + + + + + inhibitConsoleOutput + + + + true + + + + + + + + signJar + + + + + + + + org.apache.maven.plugins + + maven-gpg-plugin + + ${maven-gpg-plugin.version} + + + + + + sign-artifacts + + verify + + + + sign + + + + + + + + + + + + + + + + + + releaseToMaven + + + + + + + + org.sonatype.plugins + + nexus-staging-maven-plugin + + ${nexus-staging-maven-plugin.version} + + true + + + + ossrh + + https://oss.sonatype.org/ + + false + + + + + + + + org.apache.maven.plugins + + maven-source-plugin + + ${maven-source-plugin.version} + + + + + + attach-sources + + + + jar-no-fork + + + + + + + + + + + + org.apache.maven.plugins + + maven-javadoc-plugin + + ${maven-javadoc-plugin.version} + + + + + + attach-javadocs + + + + jar + + + + + + + + + + + + + + + + + + + + + + ossrh + + https://oss.sonatype.org/content/repositories/snapshots + + + + + + From 28d663717d385870ee646659fce40e6fc1f9cbcb Mon Sep 17 00:00:00 2001 From: Giuseppe Valente Date: Mon, 21 Aug 2023 16:09:14 +0200 Subject: [PATCH 130/228] Added jackson module on the project --- .vscode/settings.json | 2 +- .../linguafranca/pwdb/kdbx/jackson/App.java | 13 - .../pwdb/kdbx/jackson/JacksonDatabase.java | 44 ++- .../pwdb/kdbx/jackson/JacksonEntry.java | 91 ++++- .../pwdb/kdbx/jackson/JacksonGroup.java | 99 +++-- .../pwdb/kdbx/jackson/JacksonHistory.java | 29 +- .../pwdb/kdbx/jackson/JacksonIcon.java | 10 +- .../jackson/JacksonSerializableDatabase.java | 146 +++++-- .../pwdb/kdbx/jackson/KeePassFile.java | 299 --------------- .../converter/Base64ToByteConverter.java | 21 +- .../converter/Base64ToUUIDConverter.java | 35 ++ .../converter/BooleanToStringConverter.java | 30 ++ .../converter/ByteToBase64Converter.java | 26 +- .../converter/DateToStringConverter.java | 32 ++ .../converter/StringToBooleanConverter.java | 29 ++ .../converter/StringToDateConverter.java | 41 ++ .../kdbx/jackson/converter/TimeConverter.java | 18 - .../converter/UUIDToBase64Converter.java | 35 ++ .../jackson/converter/ValueDeserialized.java | 95 +++++ .../jackson/converter/ValueSerializer.java | 82 ++++ .../pwdb/kdbx/jackson/model/EntryClasses.java | 80 ++-- .../pwdb/kdbx/jackson/model/KeePassFile.java | 361 ++++++++++++++++++ .../pwdb/kdbx/jackson/model/Times.java | 30 +- .../pwdb/JacksonBinaryPropertyV3Test.java | 66 ++++ .../pwdb/JacksonBinaryPropertyV4Test.java | 66 ++++ .../pwdb/JacksonDatabaseLoadTest.java | 15 + .../pwdb/JacksonDatabaseLoaderTest.java | 34 ++ .../pwdb/JacksonDatabaseTest.java | 15 + .../pwdb/JacksonRecycleBinTest.java | 31 ++ .../pwdb/JacksonSaveAndReloadTest.java | 87 +++++ 30 files changed, 1467 insertions(+), 495 deletions(-) delete mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/App.java delete mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/KeePassFile.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToUUIDConverter.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/BooleanToStringConverter.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/DateToStringConverter.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/StringToBooleanConverter.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/StringToDateConverter.java delete mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/TimeConverter.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/UUIDToBase64Converter.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserialized.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java create mode 100644 jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java create mode 100644 jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV3Test.java create mode 100644 jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV4Test.java create mode 100644 jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoaderTest.java create mode 100644 jackson/src/test/java/org/linguafranca/pwdb/JacksonRecycleBinTest.java create mode 100644 jackson/src/test/java/org/linguafranca/pwdb/JacksonSaveAndReloadTest.java diff --git a/.vscode/settings.json b/.vscode/settings.json index c60d6d41..0518b0a9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { "java.compile.nullAnalysis.mode": "automatic", - "maven.view": "hierarchical" + "maven.view": "flat" } \ No newline at end of file diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/App.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/App.java deleted file mode 100644 index 76a27f0f..00000000 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/App.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.linguafranca.pwdb.kdbx.jackson; - -/** - * Hello world! - * - */ -public class App -{ - public static void main( String[] args ) - { - System.out.println( "Hello World!" ); - } -} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java index bc092364..1c15344c 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java @@ -21,20 +21,23 @@ import java.io.OutputStream; import java.util.Date; import java.util.List; +import java.util.Objects; import java.util.UUID; import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.StreamConfiguration; import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.base.AbstractDatabase; +import org.linguafranca.pwdb.kdbx.KdbxHeader; import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; +import org.linguafranca.pwdb.kdbx.jackson.model.KeePassFile; import com.fasterxml.jackson.core.exc.StreamReadException; import com.fasterxml.jackson.databind.DatabindException; import static org.linguafranca.pwdb.kdbx.jackson.JacksonSerializableDatabase.createEmptyDatabase;; -public class JacksonDatabase extends AbstractDatabase{ +public class JacksonDatabase extends AbstractDatabase { KeePassFile keePassFile; StreamFormat streamFormat; @@ -53,6 +56,7 @@ public JacksonDatabase(KeePassFile file, StreamFormat streamFormat) { throw new IllegalStateException(e); } } + /** * Load plaintext XML * @@ -61,7 +65,7 @@ public JacksonDatabase(KeePassFile file, StreamFormat streamFormat) { * @throws Exception on load failure */ public static JacksonDatabase loadXml(InputStream inputStream) throws Exception { - + KeePassFile keePassFile = new JacksonSerializableDatabase().load(inputStream).keePassFile; keePassFile.root.group.uuid = UUID.randomUUID(); return new JacksonDatabase(keePassFile, null); @@ -82,35 +86,36 @@ public static JacksonDatabase load(Credentials credentials, InputStream inputStr } /** - * Save the database with the same stream format that it was loaded with, or V4 default if none - * @param credentials credentials to use + * Save the database with the same stream format that it was loaded with, or V4 + * default if none + * + * @param credentials credentials to use * @param outputStream where to write to */ @Override public void save(Credentials credentials, OutputStream outputStream) throws IOException { - // save with the stream format used to load if it exists, otherwise save V4 - // if (Objects.isNull(streamFormat)) { - // streamFormat = new KdbxStreamFormat(new KdbxHeader(4)); - // } + if (Objects.isNull(streamFormat)) { + streamFormat = new KdbxStreamFormat(new KdbxHeader(4)); + } save(streamFormat, credentials, outputStream); } /** * Save the database with a choice of stream format + * * @param streamFormat the format to use - * @param credentials credentials to use + * @param credentials credentials to use * @param outputStream where to write to */ @Override public void save(StreamFormat streamFormat, Credentials credentials, - OutputStream outputStream) throws IOException{ - // keePassFile.meta.generator = "KeePassJava2-Simple"; - // SimpleSerializableDatabase simpleSerializableDatabase = new SimpleSerializableDatabase(this.keePassFile); - // streamFormat.save(simpleSerializableDatabase, credentials, outputStream); - // setDirty(false); + OutputStream outputStream) throws IOException { + keePassFile.meta.generator = "KeePassJava2-Jackson"; + JacksonSerializableDatabase simpleSerializableDatabase = new JacksonSerializableDatabase(this.keePassFile); + streamFormat.save(simpleSerializableDatabase, credentials, outputStream); + setDirty(false); } - @Override public JacksonGroup getRootGroup() { return keePassFile.root.group; @@ -190,17 +195,16 @@ public boolean shouldProtect(String s) { return keePassFile.meta.memoryProtection.shouldProtect(s); } - public List getBinaries() { - return keePassFile.getBinaries(); + return keePassFile.meta.binaries; } - public void addBinary(byte [] bytes, int index) { - // SimpleSerializableDatabase.addBinary(this.keePassFile, index, bytes); + public void addBinary(byte[] bytes, int index) { + JacksonSerializableDatabase.addBinary(this.keePassFile, index, bytes); } public StreamFormat getStreamFormat() { return streamFormat; } - + } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java index 0944c944..797f8fe1 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java @@ -22,54 +22,92 @@ import java.util.UUID; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; + import org.linguafranca.pwdb.base.AbstractEntry; import org.linguafranca.pwdb.kdbx.Helpers; +import org.linguafranca.pwdb.kdbx.jackson.converter.Base64ToUUIDConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.UUIDToBase64Converter; - -import static org.linguafranca.pwdb.kdbx.jackson.model.EntryClasses.*; +import org.linguafranca.pwdb.kdbx.jackson.model.KeePassFile; import org.linguafranca.pwdb.kdbx.jackson.model.Times; +import org.linguafranca.pwdb.kdbx.jackson.model.EntryClasses.AutoType; +import org.linguafranca.pwdb.kdbx.jackson.model.EntryClasses.BinaryProperty; +import org.linguafranca.pwdb.kdbx.jackson.model.EntryClasses.StringProperty; +import static org.linguafranca.pwdb.kdbx.jackson.model.EntryClasses.*; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; -@JacksonXmlRootElement(localName = "Entry") + + +@JsonPropertyOrder({ + "uuid", + "iconID", + "customIconUUID", + "foregroundColor", + "backgroundColor", + "overrideURL", + "tags", + "times", + "string", + "binary", + "autoType", + "history", + //"customData" +}) + + +@JsonIgnoreProperties({"path", "username", "title", "notes", "url", "password"}) public class JacksonEntry extends AbstractEntry { + @JacksonXmlProperty(localName = "UUID") + @JsonDeserialize(converter = Base64ToUUIDConverter.class) + @JsonSerialize(converter = UUIDToBase64Converter.class) protected UUID uuid; + @JacksonXmlProperty(localName = "IconID") protected int iconID; + @JacksonXmlProperty(localName = "CustomIconUUID") + @JsonDeserialize(converter = Base64ToUUIDConverter.class) + @JsonSerialize(converter = UUIDToBase64Converter.class) protected UUID customIconUUID; + @JacksonXmlProperty(localName = "ForegroundColor") protected String foregroundColor; + @JacksonXmlProperty(localName = "BackgroundColor") protected String backgroundColor; + @JacksonXmlProperty(localName = "OverrideURL") protected String overrideURL; + @JacksonXmlProperty(localName = "Tags") protected String tags; + @JacksonXmlProperty(localName = "Times") protected Times times; - @JacksonXmlProperty(localName = "String") /** Workaround jackson **/ + @JacksonXmlProperty(localName = "String") /** Workaround jackson **/ @JacksonXmlElementWrapper(useWrapping = false) protected List string; - @JacksonXmlProperty(localName = "Binary") /** Workaround jackson **/ + @JacksonXmlProperty(localName = "Binary") /** Workaround jackson **/ @JacksonXmlElementWrapper(useWrapping = false) protected List binary; @JacksonXmlProperty(localName = "AutoType") protected AutoType autoType; - @JacksonXmlProperty(localName = "History") /** Workaround jackson **/ - @JacksonXmlElementWrapper(useWrapping = false) - protected List history; + @JacksonXmlProperty(localName = "History") /** Workaround jackson **/ + protected JacksonHistory history; @JsonIgnore JacksonDatabase database; @@ -85,24 +123,25 @@ protected JacksonEntry() { iconID = 0; } - public static JacksonEntry createEntry(JacksonDatabase database) { JacksonEntry result = new JacksonEntry(); result.database = database; result.parent = null; // avoiding setProperty as it does a touch(); - for (String p: STANDARD_PROPERTY_NAMES) { + for (String p : STANDARD_PROPERTY_NAMES) { result.string.add(new StringProperty(p, new StringProperty.Value(""))); } return result; } @Override + @JsonIgnore public String getProperty(String s) { return getStringContent(getStringProperty(s, string)); } @Override + @JsonIgnore public void setProperty(String s, String s1) { StringProperty sp; if ((sp = getStringProperty(s, string)) != null) { @@ -113,8 +152,10 @@ public void setProperty(String s, String s1) { } @Override + @JsonIgnore public boolean removeProperty(String name) throws IllegalArgumentException { - if (STANDARD_PROPERTY_NAMES.contains(name)) throw new IllegalArgumentException("may not remove property: " + name); + if (STANDARD_PROPERTY_NAMES.contains(name)) + throw new IllegalArgumentException("may not remove property: " + name); StringProperty sp = getStringProperty(name, string); if (sp == null) { @@ -127,9 +168,10 @@ public boolean removeProperty(String name) throws IllegalArgumentException { } @Override + @JsonIgnore public List getPropertyNames() { List result = new ArrayList<>(); - for (StringProperty property: this.string) { + for (StringProperty property : this.string) { result.add(property.getKey()); } return result; @@ -158,13 +200,13 @@ public byte[] getBinaryProperty(String s) { public void setBinaryProperty(String s, byte[] bytes) { // remove old binary property with same name BinaryProperty bp = getBinaryProp(s, binary); - if (bp != null){ + if (bp != null) { binary.remove(bp); } // what is the next free index in the binary store? Integer max = -1; - for (KeePassFile.Binary binary: database.getBinaries()){ + for (KeePassFile.Binary binary : database.getBinaries()) { if (binary.getId() > max) { max = binary.getId(); } @@ -184,6 +226,7 @@ public void setBinaryProperty(String s, byte[] bytes) { } @Override + @JsonIgnore public boolean removeBinaryProperty(String name) throws UnsupportedOperationException { BinaryProperty bp = getBinaryProp(name, binary); if (bp != null) { @@ -195,9 +238,10 @@ public boolean removeBinaryProperty(String name) throws UnsupportedOperationExce } @Override + @JsonIgnore public List getBinaryPropertyNames() { List result = new ArrayList<>(); - for (BinaryProperty property: this.binary) { + for (BinaryProperty property : this.binary) { result.add(property.getKey()); } return result; @@ -224,46 +268,53 @@ public void setIcon(JacksonIcon icon) { } @Override + @JsonIgnore public Date getLastAccessTime() { return times.getLastAccessTime(); } @Override + @JsonIgnore public Date getCreationTime() { return times.getCreationTime(); } @Override + @JsonIgnore public boolean getExpires() { return times.getExpires(); } @Override + @JsonIgnore public void setExpires(boolean expires) { times.setExpires(expires); } @Override + @JsonIgnore public Date getExpiryTime() { return times.getExpiryTime(); } @Override + @JsonIgnore public void setExpiryTime(Date expiryTime) throws IllegalArgumentException { - if (expiryTime == null) throw new IllegalArgumentException("expiryTime may not be null"); + if (expiryTime == null) + throw new IllegalArgumentException("expiryTime may not be null"); times.setExpiryTime(expiryTime); } @Override + @JsonIgnore public Date getLastModificationTime() { return times.getLastModificationTime(); } @Override + @JsonIgnore protected void touch() { this.times.setLastModificationTime(new Date()); this.database.setDirty(true); } - - } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java index a8dfd115..f9fcc322 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java @@ -14,7 +14,6 @@ * limitations under the License. */ - package org.linguafranca.pwdb.kdbx.jackson; import java.util.ArrayList; @@ -23,57 +22,104 @@ import java.util.UUID; import org.jetbrains.annotations.NotNull; +import org.linguafranca.pwdb.kdbx.jackson.converter.StringToBooleanConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.UUIDToBase64Converter; +import org.linguafranca.pwdb.kdbx.jackson.converter.Base64ToUUIDConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.BooleanToStringConverter; import org.linguafranca.pwdb.kdbx.jackson.model.Times; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; - -@JacksonXmlRootElement(localName = "Group") -public class JacksonGroup extends org.linguafranca.pwdb.base.AbstractGroup { +// @JacksonXmlRootElement(localName = "Group") +@JsonPropertyOrder({ +"uuid", +"name", +"notes", +"iconID", +"customIconUUID", +"times", +"isExpanded", +"defaultAutoTypeSequence", +"enableAutoType", +"enableSearching", +"lastTopVisibleEntry", +"entry", +"group", +//"customData" +}) +@JsonIgnoreProperties(ignoreUnknown = true) +public class JacksonGroup + extends org.linguafranca.pwdb.base.AbstractGroup { @JacksonXmlProperty(localName = "UUID") + @JsonDeserialize(converter = Base64ToUUIDConverter.class) + @JsonSerialize(converter = UUIDToBase64Converter.class) protected UUID uuid; - + @JacksonXmlProperty(localName = "Name") protected String name; + @JacksonXmlProperty(localName = "Notes") protected String notes; + @JacksonXmlProperty(localName = "IconID") protected int iconID; + @JacksonXmlProperty(localName = "CustomIconUUID") + @JsonDeserialize(converter = Base64ToUUIDConverter.class) + @JsonSerialize(converter = UUIDToBase64Converter.class) protected UUID customIconUuid; + @JacksonXmlProperty(localName = "Times") protected Times times; + @JacksonXmlProperty(localName = "IsExpanded") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) protected Boolean isExpanded; + @JacksonXmlProperty(localName = "DefaultAutoTypeSequence") protected String defaultAutoTypeSequence; + @JacksonXmlProperty(localName = "EnableAutoType") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) protected Boolean enableAutoType; + @JacksonXmlProperty(localName = "EnableSearching") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) protected Boolean enableSearching; + @JacksonXmlProperty(localName = "LastTopVisibleEntry") + @JsonDeserialize(converter = Base64ToUUIDConverter.class) + @JsonSerialize(converter = UUIDToBase64Converter.class) protected UUID lastTopVisibleEntry; - @JacksonXmlProperty(localName = "Entry") /** Workaround jackson **/ + @JacksonXmlProperty(localName = "Entry") /** Workaround jackson **/ @JacksonXmlElementWrapper(useWrapping = false) - protected List entry; + protected List entries; - @JacksonXmlProperty(localName = "Group") /** Workaround jackson **/ + + @JacksonXmlProperty(localName = "Group") /** Workaround jackson **/ @JacksonXmlElementWrapper(useWrapping = false) - protected List group; + protected List groups; @JsonIgnore protected JacksonDatabase database; + @JsonIgnore protected JacksonGroup parent; protected JacksonGroup() { - entry = new ArrayList<>(); - group = new ArrayList<>(); + entries = new ArrayList<>(); + groups = new ArrayList<>(); times = new Times(); } @@ -121,15 +167,16 @@ public void setParent(JacksonGroup group) { @Override public List getGroups() { List result = new ArrayList<>(); - for (JacksonGroup aGroup : group) { + for (JacksonGroup aGroup : groups) { result.add(aGroup); } return result; } @Override + @JsonIgnore public int getGroupsCount() { - return group.size(); + return groups.size(); } @Override @@ -144,7 +191,7 @@ public JacksonGroup addGroup(JacksonGroup group) { group.getParent().removeGroup(group); } group.parent = this; - this.group.add(group); + this.groups.add(group); touch(); return group; } @@ -154,7 +201,7 @@ public JacksonGroup removeGroup(JacksonGroup group) { if (this.database != group.database) { throw new IllegalStateException("Must be from same database"); } - this.group.remove(group); + this.groups.remove(group); group.parent = null; touch(); return group; @@ -163,15 +210,16 @@ public JacksonGroup removeGroup(JacksonGroup group) { @Override public List getEntries() { List result = new ArrayList<>(); - for (JacksonEntry entry: this.entry){ + for (JacksonEntry entry : this.entries) { result.add(entry); } return result; } @Override + @JsonIgnore public int getEntriesCount() { - return this.entry.size(); + return this.entries.size(); } @Override @@ -182,8 +230,8 @@ public JacksonEntry addEntry(JacksonEntry entry) { if (entry.getParent() != null) { entry.getParent().removeEntry(entry); } - this.entry.add(entry); - entry.parent=this; + this.entries.add(entry); + entry.parent = this; touch(); return entry; } @@ -193,7 +241,7 @@ public JacksonEntry removeEntry(JacksonEntry entry) { if (this.database != entry.database) { throw new IllegalStateException("Must be from same database"); } - this.entry.remove(entry); + this.entries.remove(entry); entry.parent = null; return entry; } @@ -232,14 +280,13 @@ public JacksonDatabase getDatabase() { } private void touch() { - if(this.times != null) { + if (this.times != null) { this.times.setLastModificationTime(new Date()); } - - if(this.database != null) { + + if (this.database != null) { this.database.setDirty(true); } - + } } - diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonHistory.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonHistory.java index d3b7c9ea..dd9d6e97 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonHistory.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonHistory.java @@ -1,20 +1,35 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.linguafranca.pwdb.kdbx.jackson; +import java.util.ArrayList; import java.util.List; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; -@JacksonXmlRootElement(localName = "History") +// @JacksonXmlRootElement(localName = "History") public class JacksonHistory { - - @JacksonXmlProperty(localName = "Entry") /** Workaround jackson **/ + + @JacksonXmlProperty(localName = "Entry") /** Workaround jackson **/ @JacksonXmlElementWrapper(useWrapping = false) private List entry; public JacksonHistory() { - + entry = new ArrayList(); } /** @@ -31,8 +46,4 @@ public void setEntry(List entry) { this.entry = entry; } - - - - } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonIcon.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonIcon.java index d6e220ee..7e3cb732 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonIcon.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonIcon.java @@ -35,7 +35,7 @@ public JacksonIcon() { @Override public int getIndex() { - return this.index; + return this.index; } @Override @@ -45,8 +45,10 @@ public void setIndex(int index) { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; JacksonIcon that = (JacksonIcon) o; @@ -58,5 +60,5 @@ public boolean equals(Object o) { public int hashCode() { return index; } - + } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java index 967080df..912eb662 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java @@ -1,33 +1,64 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.linguafranca.pwdb.kdbx.jackson; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.Objects; + +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamWriter; import org.linguafranca.pwdb.SerializableDatabase; +import org.linguafranca.pwdb.kdbx.Helpers; +import org.linguafranca.pwdb.kdbx.jackson.converter.ValueDeserialized; +import org.linguafranca.pwdb.kdbx.jackson.converter.ValueSerializer; +import org.linguafranca.pwdb.kdbx.jackson.model.EntryClasses; +import org.linguafranca.pwdb.kdbx.jackson.model.KeePassFile; import org.linguafranca.pwdb.security.StreamEncryptor; +import com.ctc.wstx.api.WstxInputProperties; +import com.ctc.wstx.api.WstxOutputProperties; import com.fasterxml.jackson.core.exc.StreamReadException; import com.fasterxml.jackson.databind.DatabindException; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; -public class JacksonSerializableDatabase implements SerializableDatabase{ +public class JacksonSerializableDatabase implements SerializableDatabase { public KeePassFile keePassFile; - private StreamEncryptor encryption; - + private StreamEncryptor encryptor; public static KeePassFile createEmptyDatabase() throws StreamReadException, DatabindException, IOException { - InputStream inputStream = JacksonSerializableDatabase.class.getClassLoader().getResourceAsStream("base.kdbx.xml"); + InputStream inputStream = JacksonSerializableDatabase.class.getClassLoader() + .getResourceAsStream("base.kdbx.xml"); XmlMapper mapper = new XmlMapper(); KeePassFile res = mapper.readValue(inputStream, KeePassFile.class); return res; } - public JacksonSerializableDatabase(){ + public JacksonSerializableDatabase() { } + public JacksonSerializableDatabase(KeePassFile keePassFile) { this.keePassFile = keePassFile; } @@ -35,69 +66,122 @@ public JacksonSerializableDatabase(KeePassFile keePassFile) { @Override public JacksonSerializableDatabase load(InputStream inputStream) throws IOException { XmlMapper mapper = new XmlMapper(); + SimpleModule module = new SimpleModule(); + module.addDeserializer(EntryClasses.StringProperty.Value.class, new ValueDeserialized(encryptor)); + mapper.registerModule(module); keePassFile = mapper.readValue(inputStream, KeePassFile.class); return this; } - @Override - public void save(OutputStream outputStream) throws IOException { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'save'"); - } - - @Override - public StreamEncryptor getEncryption() { - return this.encryption; - } @Override - public void setEncryption(StreamEncryptor encryption) { - this.encryption = encryption; + public void save(OutputStream outputStream) throws IOException { + + try { + + XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newFactory(); + XmlMapper mapper = new XmlMapper(); + SimpleModule module = new SimpleModule(); + module.addSerializer(EntryClasses.StringProperty.Value.class, new ValueSerializer(encryptor)); + mapper.registerModule(module); + mapper.enable(ToXmlGenerator.Feature.WRITE_XML_DECLARATION); + mapper.enable(SerializationFeature.INDENT_OUTPUT); + xmlOutputFactory.setProperty(WstxOutputProperties.P_USE_DOUBLE_QUOTES_IN_XML_DECL, true); + xmlOutputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, false); + xmlOutputFactory.setProperty(WstxInputProperties.P_RETURN_NULL_FOR_DEFAULT_NAMESPACE, true); + + OutputStreamWriter osw = new OutputStreamWriter(outputStream); + XMLStreamWriter sw = xmlOutputFactory.createXMLStreamWriter(osw); + sw.setPrefix("xml", "http://www.w3.org/XML/1998/namespace"); + + mapper.writeValue(sw, keePassFile); + + sw.writeEndDocument(); + sw.close(); + + //sw.writeStartElement("root"); + + + //sw.writeComment("Some insightful commentary here"); + //sw.writeEndElement(); + // sw.writeEndDocument(); + + + } catch(Exception e) { + throw new IllegalStateException(e); + } } + @Override public byte[] getHeaderHash() { - return keePassFile.meta.headerHash; } + return keePassFile.meta.headerHash; + } @Override public void setHeaderHash(byte[] hash) { this.keePassFile.meta.headerHash = hash; } - @Override - public void addBinary(int index, byte[] payload) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'addBinary'"); + + public static void addBinary(KeePassFile keePassFile, int index, byte[] payload) { + KeePassFile.Binary newBin = new KeePassFile.Binary(); + newBin.setId(index); + newBin.setValue(Helpers.encodeBase64Content(payload, true)); + newBin.setCompressed(true); + if (keePassFile.meta.binaries == null) { + keePassFile.createBinaries(); + } + keePassFile.meta.binaries.add(newBin); } @Override public byte[] getBinary(int index) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getBinary'"); + KeePassFile.Binary binary = keePassFile.meta.binaries.get(index); + String value = binary.getValue(); + return Helpers.decodeBase64Content(value.getBytes(), binary.getCompressed()); } @Override public int getBinaryCount() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getBinaryCount'"); + if (Objects.isNull(keePassFile.meta.binaries)){ + return 0; + } + return keePassFile.meta.binaries.size(); } /** * On load add parents + * * @param parent a parent to recurse */ - static void fixUp(JacksonGroup parent){ - - for (JacksonGroup group: parent.group) { + static void fixUp(JacksonGroup parent) { + + for (JacksonGroup group : parent.groups) { group.parent = parent; group.database = parent.database; fixUp(group); } - for (JacksonEntry entry: parent.entry) { + for (JacksonEntry entry : parent.entries) { entry.database = parent.database; entry.parent = parent; } } - + + @Override + public StreamEncryptor getEncryption() { + return this.encryptor; + } + + @Override + public void setEncryption(StreamEncryptor encryption) { + this.encryptor = encryption; + } + + @Override + public void addBinary(int index, byte[] payload) { + addBinary(keePassFile, index, payload); + } + } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/KeePassFile.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/KeePassFile.java deleted file mode 100644 index c7512b4d..00000000 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/KeePassFile.java +++ /dev/null @@ -1,299 +0,0 @@ -/* - * Copyright 2023 Giuseppe Valente - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.pwdb.kdbx.jackson; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.UUID; - -import org.linguafranca.pwdb.Entry; -import org.linguafranca.pwdb.kdbx.jackson.converter.Base64ToByteConverter; -import org.linguafranca.pwdb.kdbx.jackson.converter.ByteToBase64Converter; -import org.linguafranca.pwdb.kdbx.jackson.converter.TimeConverter; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText; - -@JacksonXmlRootElement(localName = "KeePassFile") -public class KeePassFile { - - @JacksonXmlProperty(localName = "Meta") - public Meta meta; - @JacksonXmlProperty(localName = "Root") - public Root root; - - public List getBinaries() { - return meta.binaries; - } - - public void createBinaries() { - meta.binaries = new ArrayList<>(); - } - - public static class Root { - - public Root() {} - - @JacksonXmlProperty(localName = "Group") - public JacksonGroup group; - - @JacksonXmlProperty(localName = "DeletedObjects") /** Jackson workaround see https://github.com/FasterXML/jackson-dataformat-xml/issues/181#issuecomment-349411792 **/ - @JacksonXmlElementWrapper(useWrapping = false) - protected ArrayList deletedObjects; - - } - - public static class Meta { - - public Meta() {} - @JacksonXmlProperty(localName = "Generator") - public String generator; - - @JacksonXmlProperty(localName = "HeaderHash") - //public KeePassFile.ByteArray headerHash; - //public String headerHash; - @JsonDeserialize(converter = Base64ToByteConverter.class) - @JsonSerialize(converter = ByteToBase64Converter.class) - public byte[] headerHash; - - @JacksonXmlProperty(localName = "DatabaseName") - public String databaseName; - - @JacksonXmlProperty(localName = "DatabaseNameChanged") - @JsonDeserialize(converter = TimeConverter.class) - public Date databaseNameChanged; - - @JacksonXmlProperty(localName = "DatabaseDescription") - public String databaseDescription; - - @JacksonXmlProperty(localName = "DatabaseDescriptionChanged") - @JsonDeserialize(converter = TimeConverter.class) - public Date databaseDescriptionChanged; - - @JacksonXmlProperty(localName = "DefaultUserName") - protected String defaultUserName; - - @JacksonXmlProperty(localName = "DefaultUserNameChanged") - @JsonDeserialize(converter = TimeConverter.class) - protected Date defaultUserNameChanged; - - @JacksonXmlProperty(localName = "MaintenanceHistoryDays") - protected int maintenanceHistoryDays; - - @JacksonXmlProperty(localName = "Color") - protected String color; - - @JacksonXmlProperty(localName = "MasterKeyChanged") - @JsonDeserialize(converter = TimeConverter.class) - protected Date masterKeyChanged; - - @JacksonXmlProperty(localName = "MasterKeyChangeRec") - protected int masterKeyChangeRec; - - @JacksonXmlProperty(localName = "MasterKeyChangeForce") - protected int masterKeyChangeForce; - - @JacksonXmlProperty(localName = "MemoryProtection") - public KeePassFile.MemoryProtection memoryProtection; - - @JacksonXmlElementWrapper(localName = "CustomIcons") - protected ArrayList customIcons; - @JacksonXmlProperty(localName = "RecycleBinEnabled") - public Boolean recycleBinEnabled; - - @JacksonXmlProperty(localName = "RecycleBinUUID") - public UUID recycleBinUUID; - - @JacksonXmlProperty(localName = "RecycleBinChanged") - @JsonDeserialize(converter = TimeConverter.class) - public Date recycleBinChanged; - - @JacksonXmlProperty(localName = "EntryTemplatesGroup") - - protected UUID entryTemplatesGroup; - - @JacksonXmlProperty(localName = "EntryTemplatesGroupChanged") - @JsonDeserialize(converter = TimeConverter.class) - protected Date entryTemplatesGroupChanged; - - @JacksonXmlProperty(localName = "LastSelectedGroup") - - protected UUID lastSelectedGroup; - @JacksonXmlProperty(localName = "LastTopVisibleGroup") - - protected UUID lastTopVisibleGroup; - @JacksonXmlProperty(localName = "HistoryMaxItems") - protected int historyMaxItems; - @JacksonXmlProperty(localName = "HistoryMaxSize") - protected int historyMaxSize; - - @JacksonXmlElementWrapper(localName = "Binaries") - protected List binaries; - - @JacksonXmlProperty(localName = "CustomData") - protected KeePassFile.CustomData customData; - - /* version 4 */ - - @JacksonXmlProperty(localName = "SettingsChanged") - @JsonDeserialize(converter = TimeConverter.class) - protected Date settingsChanged; - } - - - public static class MemoryProtection { - public MemoryProtection() {} - - @JacksonXmlProperty(localName = "ProtectTitle") - - protected Boolean protectTitle; - @JacksonXmlProperty(localName = "ProtectUserName") - - protected Boolean protectUserName; - @JacksonXmlProperty(localName = "ProtectPassword") - - protected Boolean protectPassword; - @JacksonXmlProperty(localName = "ProtectURL") - - protected Boolean protectURL; - @JacksonXmlProperty(localName = "ProtectNotes") - - protected Boolean protectNotes; - - public Boolean shouldProtect(String name) { - switch(name) { - case Entry.STANDARD_PROPERTY_NAME_TITLE: return protectTitle; - case Entry.STANDARD_PROPERTY_NAME_USER_NAME: return protectUserName; - case Entry.STANDARD_PROPERTY_NAME_PASSWORD: return protectPassword; - case Entry.STANDARD_PROPERTY_NAME_URL: return protectURL; - case Entry.STANDARD_PROPERTY_NAME_NOTES: return protectNotes; - } - return false; - } - } - - public static class Binaries { - public Binaries() {} - } - - @JacksonXmlRootElement(localName = "Binary") - public static class Binary { - - public Binary() {} - - @JacksonXmlText - protected String value; - - @JacksonXmlProperty(localName = "ID", isAttribute = true) - protected Integer id; - - @JacksonXmlProperty(localName = "Compressed", isAttribute = true) - protected Boolean compressed; - - public String getName() { - return String.valueOf(id); - } - - public String getValue() { - return value; - } - - public Boolean getCompressed() { - return compressed; - } - - public Integer getId() { - return id; - } - - public void setId(Integer Id) { - this.id = Id; - } - - public void setValue(String value) { - this.value = value; - } - - public void setCompressed(boolean compressed) { - this.compressed = compressed; - } - } - - @JacksonXmlRootElement(localName = "Icon") - public static class Icon { - public Icon() {} - @JacksonXmlProperty(localName = "UUID") - protected UUID uuid; - @JacksonXmlProperty(localName = "Name") - protected String name; - @JacksonXmlProperty(localName = "LastModificationTime") - @JsonDeserialize(converter = TimeConverter.class) - protected Date lastModificationTime; - @JacksonXmlProperty(localName = "Data") - @JsonDeserialize(converter = Base64ToByteConverter.class) - @JsonSerialize(converter = ByteToBase64Converter.class) - protected byte[] data; - - public String getName() { - return uuid.toString(); - } - } - - /* public static class ByteArray { - - public ByteArray() { - - } - - private byte[] content; - - public ByteArray(byte[] content) { - this.content = content; - } - - public byte[] getContent() { - return content; - } - - public void setContent(byte[] content) { - this.content = content; - } - } */ - - public static class CustomData { - - public CustomData() {} - - protected List any; - } - - @JacksonXmlRootElement(localName = "DeletedObjects") - public static class DeletedObject { - public DeletedObject() {} - - @JacksonXmlProperty(localName = "UUID") - protected UUID uuid; - @JacksonXmlProperty(localName = "DeletionTime") - @JsonDeserialize(converter = TimeConverter.class) - protected Date deletionTime; - } -} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToByteConverter.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToByteConverter.java index 355041a5..feb67339 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToByteConverter.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToByteConverter.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.linguafranca.pwdb.kdbx.jackson.converter; import java.util.Base64; @@ -11,9 +27,4 @@ public byte[] convert(String value) { byte[] decode = Base64.getDecoder().decode(value); return decode; } - - - - - } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToUUIDConverter.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToUUIDConverter.java new file mode 100644 index 00000000..c77c267f --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToUUIDConverter.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx.jackson.converter; + +import java.util.UUID; + +import org.linguafranca.pwdb.kdbx.Helpers; + +import com.fasterxml.jackson.databind.util.StdConverter; + +public class Base64ToUUIDConverter extends StdConverter { + + @Override + public UUID convert(String value) { + if(value == null || value.equals("")) { + return UUID.randomUUID(); + } + return Helpers.uuidFromBase64(value); + } + +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/BooleanToStringConverter.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/BooleanToStringConverter.java new file mode 100644 index 00000000..4059b1c8 --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/BooleanToStringConverter.java @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx.jackson.converter; + +import org.linguafranca.pwdb.kdbx.Helpers; + +import com.fasterxml.jackson.databind.util.StdConverter; + +public class BooleanToStringConverter extends StdConverter { + + @Override + public String convert(Boolean value) { + return Helpers.fromBoolean(value); + } + +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ByteToBase64Converter.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ByteToBase64Converter.java index 6d344dc9..fd613a9f 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ByteToBase64Converter.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ByteToBase64Converter.java @@ -1,17 +1,29 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.linguafranca.pwdb.kdbx.jackson.converter; -import java.math.BigInteger; +import java.util.Base64; import com.fasterxml.jackson.databind.util.StdConverter; - -public class ByteToBase64Converter extends StdConverter{ +public class ByteToBase64Converter extends StdConverter { @Override public String convert(byte[] value) { - String binaryStr = new BigInteger(1, value).toString(2); - return binaryStr; - + return new String(Base64.getEncoder().encode(value)); } - + } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/DateToStringConverter.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/DateToStringConverter.java new file mode 100644 index 00000000..752579c0 --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/DateToStringConverter.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx.jackson.converter; + +import java.util.Date; + +import org.linguafranca.pwdb.kdbx.Helpers; + +import com.fasterxml.jackson.databind.util.StdConverter; + +public class DateToStringConverter extends StdConverter { + + @Override + public String convert(Date date) { + return Helpers.fromDate(date); + } + +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/StringToBooleanConverter.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/StringToBooleanConverter.java new file mode 100644 index 00000000..b295edd9 --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/StringToBooleanConverter.java @@ -0,0 +1,29 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb.kdbx.jackson.converter; + +import org.linguafranca.pwdb.kdbx.Helpers; + +import com.fasterxml.jackson.databind.util.StdConverter; + +public class StringToBooleanConverter extends StdConverter{ + + @Override + public Boolean convert(String value) { + return Helpers.toBoolean(value); + } + +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/StringToDateConverter.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/StringToDateConverter.java new file mode 100644 index 00000000..d334a5bb --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/StringToDateConverter.java @@ -0,0 +1,41 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb.kdbx.jackson.converter; + +import java.util.Date; + +import org.linguafranca.pwdb.kdbx.Helpers; + +import com.fasterxml.jackson.databind.util.StdConverter; + +public class StringToDateConverter extends StdConverter { + + @Override + public Date convert(String value) { + Date result = null; + if(value != null) { + if(value.equals("${creationDate}")) { + result = new Date(); + } + try { + result = Helpers.toDate(value); + } catch(Exception e) { + result = new Date(); + } + } + return result; + } +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/TimeConverter.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/TimeConverter.java deleted file mode 100644 index ba501603..00000000 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/TimeConverter.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.linguafranca.pwdb.kdbx.jackson.converter; - -import java.util.Date; - -import org.linguafranca.pwdb.kdbx.Helpers; - -import com.fasterxml.jackson.databind.util.StdConverter; - -public class TimeConverter extends StdConverter{ - - @Override - public Date convert(String value) { - if (value.equals("${creationDate}")) { - return new Date(); - } - return Helpers.toDate(value); - } -} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/UUIDToBase64Converter.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/UUIDToBase64Converter.java new file mode 100644 index 00000000..3106bc2e --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/UUIDToBase64Converter.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb.kdbx.jackson.converter; + +import java.util.UUID; + +import org.linguafranca.pwdb.kdbx.Helpers; + +import com.fasterxml.jackson.databind.util.StdConverter; + +public class UUIDToBase64Converter extends StdConverter { + + @Override + public String convert(UUID value) { + + if(value != null) { + return Helpers.base64FromUuid(value); + } + return new String(); + } + +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserialized.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserialized.java new file mode 100644 index 00000000..e448589e --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserialized.java @@ -0,0 +1,95 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb.kdbx.jackson.converter; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.codec.binary.Base64; +import org.linguafranca.pwdb.kdbx.Helpers; +import org.linguafranca.pwdb.kdbx.jackson.model.EntryClasses; +import org.linguafranca.pwdb.security.StreamEncryptor; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +public class ValueDeserialized extends StdDeserializer { + + private StreamEncryptor encryptor; + + public ValueDeserialized() { + super(ValueDeserialized.class); + } + + public ValueDeserialized(Class v) { + super(v); + } + + public ValueDeserialized(StreamEncryptor encryptor) { + super(ValueDeserialized.class); + this.encryptor = encryptor; + } + + @Override + public EntryClasses.StringProperty.Value deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException { + + + JsonNode node = p.getCodec().readTree(p); + + EntryClasses.StringProperty.Value result = new EntryClasses.StringProperty.Value(); + + if(node.isTextual()) { + result.setText(node.textValue()); + } else if(node.isObject()) { + + //We need to decrypt all Protected values + if(node.has("Protected")) { + + //Check if Protected=True + Boolean nodeEncrypted = Helpers.toBoolean(node.get("Protected").asText()); + if(nodeEncrypted) { + if(node.has("")) { + String cipherText = node.get("").asText(); + if(cipherText != null && !cipherText.isEmpty()) { + + //Decode to byte the Base64 text + byte[] encrypted = Base64.decodeBase64(cipherText.getBytes()); + String decrypted = new String(encryptor.decrypt(encrypted), StandardCharsets.UTF_8); + result.setText(decrypted); + result.setProtectOnOutput(true); + } + } + } + } else { + //If an element is not marked us Protected we need to copy the value as is + if(node.has("ProtectInMemory")) { + Boolean protectInMemory = Helpers.toBoolean(node.get("ProtectInMemory").asText()); + result.setProtectInMemory(protectInMemory); + + if(node.has("")) { + result.setText(node.get("").asText()); + } + } + } + } + + return result; + } + +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java new file mode 100644 index 00000000..640f5db8 --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java @@ -0,0 +1,82 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb.kdbx.jackson.converter; + +import java.io.IOException; + +import org.apache.commons.codec.binary.Base64; +import org.linguafranca.pwdb.kdbx.jackson.model.EntryClasses; +import org.linguafranca.pwdb.kdbx.jackson.model.EntryClasses.StringProperty.Value; +import org.linguafranca.pwdb.security.StreamEncryptor; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; + + +public class ValueSerializer extends StdSerializer{ + + private StreamEncryptor encryptor; + + public ValueSerializer() { + super(ValueSerializer.class, false); + } + + public ValueSerializer(Class v) { + super(v); + } + + public ValueSerializer(StreamEncryptor encryptor) { + super(ValueSerializer.class, false); + this.encryptor = encryptor; + } + + + @Override + public void serialize(Value value, JsonGenerator gen, SerializerProvider provider) throws IOException { + + final ToXmlGenerator xmlGenerator = (ToXmlGenerator) gen; + xmlGenerator.writeStartObject(); + + //We need to encrypt and convert to base64 every protected element + if(value.getProtectOnOutput()) { + xmlGenerator.setNextIsAttribute(true); + gen.writeStringField("Protected", "True"); + String plain = value.getText(); + if(plain == null) { + plain = ""; + } + //Cipher + byte[] encrypted = encryptor.encrypt(plain.getBytes()); + //Convert to base64 + String base64 = new String(Base64.encodeBase64(encrypted)); + + //Destroy from memory the plain value + plain = null; + value.setText(base64); + + } + + xmlGenerator.setNextIsAttribute(false); + xmlGenerator.setNextIsUnwrapped(true); + xmlGenerator.writeStringField("text",value.getText()); + + gen.writeEndObject(); + + } + +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java index 72e2d391..e8ae12eb 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java @@ -16,19 +16,20 @@ package org.linguafranca.pwdb.kdbx.jackson.model; - import java.util.ArrayList; import java.util.List; import java.util.Objects; import org.linguafranca.pwdb.kdbx.jackson.JacksonEntry; +import org.linguafranca.pwdb.kdbx.jackson.converter.BooleanToStringConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.StringToBooleanConverter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText; - public abstract class EntryClasses { public static StringProperty getStringProperty(String name, List string) { for (StringProperty property : string) { @@ -40,7 +41,7 @@ public static StringProperty getStringProperty(String name, List } public static String getStringContent(StringProperty property) { - return property == null || property.value == null? null:property.value.text; + return property == null || property.value == null ? null : property.value.text; } public static BinaryProperty getBinaryProp(String name, List binary) { @@ -53,12 +54,14 @@ public static BinaryProperty getBinaryProp(String name, List bin } public static String getBinaryContent(BinaryProperty property) { - return property == null || property.value == null ? null : property.value.ref; + return property == null || property.value == null ? null : property.value.ref; } - @JacksonXmlRootElement(localName = "AutoType") + //@JacksonXmlRootElement(localName = "AutoType") public static class AutoType { @JacksonXmlProperty(localName = "Enabled") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) protected Boolean enabled; @JacksonXmlProperty(localName = "DataTransferObfuscation") @@ -81,12 +84,13 @@ public static class Association { } } - @JacksonXmlRootElement(localName = "String") - public static class StringProperty { + //@JacksonXmlRootElement(localName = "String") + public static class StringProperty { public StringProperty() { - this("",new Value()); + } + public StringProperty(String key, Value value) { this.key = key; this.value = value; @@ -98,10 +102,6 @@ public StringProperty(String key, Value value) { @JacksonXmlProperty(localName = "Value") Value value; - public String getName() { - return key; - } - public String getKey() { return key; } @@ -110,11 +110,19 @@ public Value getValue() { return value; } - @JacksonXmlRootElement(localName = "Value") + public void setKey(String key) { + this.key = key; + } + + public void setValue(Value value) { + this.value = value; + } + // @JsonSerialize(using = ValueBooleanSerializer.class) public static class Value { - public Value(){ - this(""); + + public Value() { } + public Value(String text) { this.text = text; this._protected = false; @@ -126,12 +134,19 @@ public Value(String text, Boolean _protected) { } @JacksonXmlProperty(localName = "ProtectInMemory", isAttribute = true) + //@JsonDeserialize(converter = StringToBooleanConverter.class) + //@JsonSerialize(converter = BooleanToStringConverter.class) protected Boolean protectInMemory; - @JacksonXmlProperty(localName = "Protected", isAttribute = true) + @JacksonXmlProperty(localName = "Protected", isAttribute = true) + //@JsonDeserialize(converter = StringToBooleanConverter.class) + //@JsonSerialize(converter = BooleanToStringConverter.class) Boolean _protected; - + + @JacksonXmlProperty(localName = "kpj2-ProtectOnOutput", isAttribute = true) + //@JsonDeserialize(converter = StringToBooleanConverter.class) + //@JsonSerialize(converter = BooleanToStringConverter.class) Boolean protectOnOutput; @JacksonXmlText @@ -153,7 +168,6 @@ public boolean getProtectOnOutput() { return Objects.nonNull(this.protectOnOutput) && this.protectOnOutput; } - public Boolean getProtected() { return _protected; } @@ -161,11 +175,21 @@ public Boolean getProtected() { public void setProtected(Boolean _protected) { this._protected = _protected; } + + + public Boolean getProtectInMemory() { + return protectInMemory; + } + + + public void setProtectInMemory(Boolean protectInMemory) { + this.protectInMemory = protectInMemory; + } } } - @JacksonXmlRootElement(localName = "Binary") - public static class BinaryProperty { + // @JacksonXmlRootElement(localName = "Binary") + public static class BinaryProperty { @JacksonXmlProperty(localName = "Key") String key; @@ -177,10 +201,6 @@ public String getKey() { return key; } - public String getName() { - return key; - } - public void setKey(String key) { this.key = key; } @@ -189,7 +209,6 @@ public void setValue(Value value) { this.value = value; } - @JacksonXmlRootElement(localName = "Value") public static class Value { @JacksonXmlProperty(localName = "Ref", isAttribute = true) String ref; @@ -202,13 +221,16 @@ public void setRef(String ref) { public static class History { - @JacksonXmlElementWrapper(localName = "JacksonEntry", useWrapping = false) + @JacksonXmlProperty(localName = "Entry") /** Workaround jackson **/ + @JacksonXmlElementWrapper(useWrapping = false) + //@JacksonXmlElementWrapper(localName = "JacksonEntry", useWrapping = false) private List list; - public History(){ + public History() { list = new ArrayList<>(); } - public List getHistory(){ + + public List getHistory() { return list; } } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java new file mode 100644 index 00000000..2e87b62b --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java @@ -0,0 +1,361 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx.jackson.model; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.linguafranca.pwdb.Entry; +import org.linguafranca.pwdb.kdbx.jackson.JacksonGroup; +import org.linguafranca.pwdb.kdbx.jackson.converter.Base64ToByteConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.Base64ToUUIDConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.BooleanToStringConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.ByteToBase64Converter; +import org.linguafranca.pwdb.kdbx.jackson.converter.DateToStringConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.StringToBooleanConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.StringToDateConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.UUIDToBase64Converter; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText; + +@JacksonXmlRootElement(localName = "KeePassFile") +public class KeePassFile { + + @JacksonXmlProperty(localName = "Meta") + public Meta meta; + @JacksonXmlProperty(localName = "Root") + public Root root; + + public void createBinaries() { + meta.binaries = new ArrayList<>(); + } + + // @JacksonXmlRootElement(localName = "Root") + @JsonPropertyOrder({ + "group", + "deletedObject" + }) + public static class Root { + + public Root() { + } + + @JacksonXmlProperty(localName = "Group") + public JacksonGroup group; + + @JacksonXmlProperty(localName = "DeletedObjects") + protected Object deletedObject; + + } + + // @JacksonXmlRootElement(localName = "Meta") + @JsonPropertyOrder({ + "generator", + "headerHash", + "databaseName", + "databaseNameChanged", + "databaseDescription", + "databaseDescriptionChanged", + "defaultUserName", + "defaultUserNameChanged", + "maintenanceHistoryDays", + "color", + "masterKeyChanged", + "masterKeyChangeRec", + "masterKeyChangeForce", + "memoryProtection", + "customIcons", + "recycleBinEnabled", + "recycleBinUUID", + "recycleBinChanged", + "entryTemplatesGroup", + "entryTemplatesGroupChanged", + "lastSelectedGroup", + "lastTopVisibleGroup", + "historyMaxItems", + "historyMaxSize", + "binaries", + //"customData" + }) + public static class Meta { + + public Meta() { + } + + @JacksonXmlProperty(localName = "Generator") + public String generator; + + @JacksonXmlProperty(localName = "HeaderHash") + @JsonDeserialize(converter = Base64ToByteConverter.class) + @JsonSerialize(converter = ByteToBase64Converter.class) + public byte[] headerHash; + + @JacksonXmlProperty(localName = "DatabaseName") + public String databaseName; + + @JacksonXmlProperty(localName = "DatabaseNameChanged") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + public Date databaseNameChanged; + + @JacksonXmlProperty(localName = "DatabaseDescription") + public String databaseDescription; + + @JacksonXmlProperty(localName = "DatabaseDescriptionChanged") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + public Date databaseDescriptionChanged; + + @JacksonXmlProperty(localName = "DefaultUserName") + protected String defaultUserName; + + @JacksonXmlProperty(localName = "DefaultUserNameChanged") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + protected Date defaultUserNameChanged; + + @JacksonXmlProperty(localName = "MaintenanceHistoryDays") + protected int maintenanceHistoryDays; + + @JacksonXmlProperty(localName = "Color") + protected String color; + + @JacksonXmlProperty(localName = "MasterKeyChanged") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + protected Date masterKeyChanged; + + @JacksonXmlProperty(localName = "MasterKeyChangeRec") + protected int masterKeyChangeRec; + + @JacksonXmlProperty(localName = "MasterKeyChangeForce") + protected int masterKeyChangeForce; + + @JacksonXmlProperty(localName = "MemoryProtection") + public KeePassFile.MemoryProtection memoryProtection; + + @JacksonXmlElementWrapper(localName = "CustomIcons") + protected ArrayList customIcons; + + @JacksonXmlProperty(localName = "RecycleBinEnabled") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) + public Boolean recycleBinEnabled; + + @JacksonXmlProperty(localName = "RecycleBinUUID") + @JsonDeserialize(converter = Base64ToUUIDConverter.class) + @JsonSerialize(converter = UUIDToBase64Converter.class) + public UUID recycleBinUUID; + + @JacksonXmlProperty(localName = "RecycleBinChanged") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + public Date recycleBinChanged; + + @JacksonXmlProperty(localName = "EntryTemplatesGroup") + @JsonDeserialize(converter = Base64ToUUIDConverter.class) + @JsonSerialize(converter = UUIDToBase64Converter.class) + protected UUID entryTemplatesGroup; + + @JacksonXmlProperty(localName = "EntryTemplatesGroupChanged") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + protected Date entryTemplatesGroupChanged; + + @JacksonXmlProperty(localName = "LastSelectedGroup") + @JsonDeserialize(converter = Base64ToUUIDConverter.class) + @JsonSerialize(converter = UUIDToBase64Converter.class) + protected UUID lastSelectedGroup; + + @JacksonXmlProperty(localName = "LastTopVisibleGroup") + @JsonDeserialize(converter = Base64ToUUIDConverter.class) + @JsonSerialize(converter = UUIDToBase64Converter.class) + protected UUID lastTopVisibleGroup; + + @JacksonXmlProperty(localName = "HistoryMaxItems") + protected int historyMaxItems; + + @JacksonXmlProperty(localName = "HistoryMaxSize") + protected int historyMaxSize; + + @JacksonXmlProperty(localName = "Binaries") + public List binaries; + + @JacksonXmlProperty(localName = "CustomData") + protected KeePassFile.CustomData customData; + + /* version 4 */ + + @JacksonXmlProperty(localName = "SettingsChanged") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + protected Date settingsChanged; + } + + @JsonPropertyOrder({ + "protectTitle", + "protectUserName", + "protectPassword", + "protectURL", + "protectNotes" + }) + public static class MemoryProtection { + public MemoryProtection() { + } + + @JacksonXmlProperty(localName = "ProtectTitle") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) + protected Boolean protectTitle; + + @JacksonXmlProperty(localName = "ProtectUserName") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) + protected Boolean protectUserName; + + @JacksonXmlProperty(localName = "ProtectPassword") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) + protected Boolean protectPassword; + + @JacksonXmlProperty(localName = "ProtectURL") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) + protected Boolean protectURL; + + @JacksonXmlProperty(localName = "ProtectNotes") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) + protected Boolean protectNotes; + + public Boolean shouldProtect(String name) { + switch (name) { + case Entry.STANDARD_PROPERTY_NAME_TITLE: + return protectTitle; + case Entry.STANDARD_PROPERTY_NAME_USER_NAME: + return protectUserName; + case Entry.STANDARD_PROPERTY_NAME_PASSWORD: + return protectPassword; + case Entry.STANDARD_PROPERTY_NAME_URL: + return protectURL; + case Entry.STANDARD_PROPERTY_NAME_NOTES: + return protectNotes; + } + return false; + } + } + + public static class Binaries { + public Binaries() { + } + } + + // @JacksonXmlRootElement(localName = "Binary") + public static class Binary { + + public Binary() { + } + + @JacksonXmlText + protected String value; + + @JacksonXmlProperty(localName = "ID", isAttribute = true) + protected Integer id; + + @JacksonXmlProperty(localName = "Compressed", isAttribute = true) + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) + protected Boolean compressed; + + public String getValue() { + return value; + } + + public Boolean getCompressed() { + return compressed; + } + + public Integer getId() { + return id; + } + + public void setId(Integer Id) { + this.id = Id; + } + + public void setValue(String value) { + this.value = value; + } + + public void setCompressed(boolean compressed) { + this.compressed = compressed; + } + } + + // @JacksonXmlRootElement(localName = "Icon") + public static class Icon { + public Icon() { + } + + @JacksonXmlProperty(localName = "UUID") + @JsonDeserialize(converter = Base64ToUUIDConverter.class) + @JsonSerialize(converter = UUIDToBase64Converter.class) + protected UUID uuid; + + @JacksonXmlProperty(localName = "Name") + protected String name; + + @JacksonXmlProperty(localName = "LastModificationTime") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + protected Date lastModificationTime; + + @JacksonXmlProperty(localName = "Data") + @JsonDeserialize(converter = Base64ToByteConverter.class) + @JsonSerialize(converter = ByteToBase64Converter.class) + protected byte[] data; + + public String getName() { + return uuid.toString(); + } + } + + // @JacksonXmlRootElement(localName = "CustomData") + public static class CustomData { + + public CustomData() { + } + + public List getAny() { + return any; + } + + public void setAny(List any) { + this.any = any; + } + + protected List any; + } +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/Times.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/Times.java index cbe668b4..80477e07 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/Times.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/Times.java @@ -16,33 +16,35 @@ package org.linguafranca.pwdb.kdbx.jackson.model; - import java.util.Date; -import org.linguafranca.pwdb.kdbx.jackson.converter.TimeConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.DateToStringConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.StringToDateConverter; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; - - -@JacksonXmlRootElement(localName = "Times") +// @JacksonXmlRootElement(localName = "Times") public class Times { @JacksonXmlProperty(localName = "LastModificationTime") - @JsonDeserialize(converter = TimeConverter.class) + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) protected Date lastModificationTime; @JacksonXmlProperty(localName = "CreationTime") - @JsonDeserialize(converter = TimeConverter.class) + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) protected Date creationTime; @JacksonXmlProperty(localName = "LastAccessTime") - @JsonDeserialize(converter = TimeConverter.class) + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) protected Date lastAccessTime; @JacksonXmlProperty(localName = "ExpiryTime") - @JsonDeserialize(converter = TimeConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + @JsonDeserialize(converter = StringToDateConverter.class) protected Date expiryTime; @JacksonXmlProperty(localName = "Expires") @@ -50,9 +52,10 @@ public class Times { @JacksonXmlProperty(localName = "UsageCount") protected int usageCount; - + @JacksonXmlProperty(localName = "LocationChanged") - @JsonDeserialize(converter = TimeConverter.class) + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) protected Date locationChanged; public Date getLastModificationTime() { @@ -112,8 +115,9 @@ public void setLocationChanged(Date locationChanged) { } public Times() { - this(new Date(System.currentTimeMillis()/1000*1000)); + this(new Date(System.currentTimeMillis() / 1000 * 1000)); } + public Times(Date date) { lastModificationTime = date; lastAccessTime = date; diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV3Test.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV3Test.java new file mode 100644 index 00000000..4760d509 --- /dev/null +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV3Test.java @@ -0,0 +1,66 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.linguafranca.pwdb.checks.BinaryPropertyChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; + +public class JacksonBinaryPropertyV3Test extends BinaryPropertyChecks { + + + public JacksonBinaryPropertyV3Test() { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment.kdbx"); + try { + database = JacksonDatabase.load(new KdbxCreds("123".getBytes()),inputStream); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + @Override + public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { + database.save(credentials, outputStream); + } + + @Override + public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + return JacksonDatabase.load(credentials, inputStream); + } + + @Override + public Database newDatabase() { + Database result = null; + try { + result = new JacksonDatabase(); + } catch(Exception e) { + e.printStackTrace(); + } + return result; + + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } + +} diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV4Test.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV4Test.java new file mode 100644 index 00000000..1ae7ecba --- /dev/null +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV4Test.java @@ -0,0 +1,66 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.linguafranca.pwdb.checks.BinaryPropertyChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; + +public class JacksonBinaryPropertyV4Test extends BinaryPropertyChecks{ + + + public JacksonBinaryPropertyV4Test() throws Exception{ + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha20-Argon2-Attachment.kdbx"); + database = JacksonDatabase.load(new KdbxCreds("123".getBytes()), inputStream); + } + + + @Override + public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { + database.save(credentials, outputStream); + } + + @Override + public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + try { + return JacksonDatabase.load(credentials, inputStream); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public Database newDatabase() { + Database result = null; + try { + result = new JacksonDatabase(); + } catch(Exception e) { + e.printStackTrace(); + } + return result; + + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } + +} diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoadTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoadTest.java index c71a67d7..2f0e4d2e 100644 --- a/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoadTest.java +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoadTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.linguafranca.pwdb; import java.io.InputStream; diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoaderTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoaderTest.java new file mode 100644 index 00000000..7c218fc3 --- /dev/null +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoaderTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb; + +import java.io.InputStream; + +import org.linguafranca.pwdb.checks.DatabaseLoaderChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; + +public class JacksonDatabaseLoaderTest extends DatabaseLoaderChecks{ + + + public JacksonDatabaseLoaderTest() throws Exception{ + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test123.kdbx"); + // file has password credentials + Credentials credentials = new KdbxCreds("123".getBytes()); + super.database = JacksonDatabase.load(credentials, inputStream); + } + +} diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseTest.java index 1f4dfdd5..cbaaa291 100644 --- a/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseTest.java +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.linguafranca.pwdb; import java.io.IOException; diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonRecycleBinTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonRecycleBinTest.java new file mode 100644 index 00000000..fc63749d --- /dev/null +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonRecycleBinTest.java @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb; + +import org.linguafranca.pwdb.checks.RecycleBinChecks; +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; + +public class JacksonRecycleBinTest extends RecycleBinChecks { + + public JacksonRecycleBinTest() { + try { + database = new JacksonDatabase(); + } catch(Exception e) { + e.printStackTrace(); + } + + } +} diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonSaveAndReloadTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonSaveAndReloadTest.java new file mode 100644 index 00000000..fcb2db76 --- /dev/null +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonSaveAndReloadTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb; + +import static org.junit.Assert.assertTrue; +import static org.linguafranca.util.TestUtil.getTestPrintStream; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; + +import org.junit.Test; +import org.linguafranca.pwdb.checks.SaveAndReloadChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.KdbxHeader; + +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; +import org.linguafranca.pwdb.kdbx.jackson.JacksonEntry; +import org.linguafranca.pwdb.kdbx.jackson.JacksonGroup; +import org.linguafranca.pwdb.kdbx.jackson.JacksonIcon; + + +public class JacksonSaveAndReloadTest extends SaveAndReloadChecks{ + + @Override + public JacksonDatabase getDatabase() { + try { + return new JacksonDatabase(); + } catch(Exception e) { + return null; + } + + } + + @Override + public JacksonDatabase getDatabase(String name, Credentials credentials) throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(name); + return JacksonDatabase.load(credentials, inputStream); + } + + @Override + public void saveDatabase(JacksonDatabase database, Credentials credentials, OutputStream outputStream) throws IOException { + database.save(credentials, outputStream); + } + + @Override + public JacksonDatabase loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + return JacksonDatabase.load(credentials, inputStream); + } + + + @Override + public boolean verifyStreamFormat(StreamFormat s1, StreamFormat s2) { + KdbxHeader h1 = (KdbxHeader) s1.getStreamConfiguration(); + KdbxHeader h2 = (KdbxHeader) s1.getStreamConfiguration(); + return (h1.getVersion() == h2.getVersion() && + h1.getProtectedStreamAlgorithm().equals(h2.getProtectedStreamAlgorithm()) && + h1.getKeyDerivationFunction().equals(h2.getKeyDerivationFunction()) && + h1.getCipherAlgorithm().equals(h2.getCipherAlgorithm())); + } + + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } + +} From c99b09a5f50042fd1073c5d32fa1df3bb7907855 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 21 Aug 2023 17:26:23 +0100 Subject: [PATCH 131/228] [maven-release-plugin] prepare release KeePassJava2-2.2 --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 7 +++---- simple/pom.xml | 2 +- test/pom.xml | 2 +- 10 files changed, 12 insertions(+), 13 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index a7f91c5f..b042c41b 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index bbe7b516..81664e1c 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index 9b425739..adfccb7c 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index 0344029f..9352f75c 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 6f4ad86d..80d6d85b 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index d0735d6e..7af226cb 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index 7c289a88..e8fce462 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index 9581086e..6d360434 100644 --- a/pom.xml +++ b/pom.xml @@ -15,13 +15,12 @@ ~ limitations under the License. --> - + 4.0.0 org.linguafranca.pwdb KeePassJava2-parent - 2.2-SNAPSHOT + 2.2 database test @@ -51,7 +50,7 @@ scm:git:git://github.com/jorabin/KeePassJava2.git scm:git:git@github.com:jorabin/KeePassJava2.git https://github.com/jorabin/KeePassJava2 - HEAD + KeePassJava2-2.2 diff --git a/simple/pom.xml b/simple/pom.xml index 504a9155..54e11631 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index f695f755..22df3951 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 4.0.0 From de36a5483659d7aad29cfdbe941d3b0101524b11 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 21 Aug 2023 17:26:27 +0100 Subject: [PATCH 132/228] [maven-release-plugin] prepare for next development iteration --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 4 ++-- simple/pom.xml | 2 +- test/pom.xml | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index b042c41b..161d9e82 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index 81664e1c..2c4bb41d 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index adfccb7c..b3175154 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index 9352f75c..9c349724 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 80d6d85b..90d1eedf 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index 7af226cb..44af7a25 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index e8fce462..49295390 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index 6d360434..d82912ce 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.2 + 2.2.1-SNAPSHOT database test @@ -50,7 +50,7 @@ scm:git:git://github.com/jorabin/KeePassJava2.git scm:git:git@github.com:jorabin/KeePassJava2.git https://github.com/jorabin/KeePassJava2 - KeePassJava2-2.2 + HEAD diff --git a/simple/pom.xml b/simple/pom.xml index 54e11631..7ce047c2 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index 22df3951..b9ba156d 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT 4.0.0 From 1fae548b9c1825714f7f6c78d59472d7b9cdb7bd Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 21 Aug 2023 17:32:55 +0100 Subject: [PATCH 133/228] Javadoc correction --- .../java/org/linguafranca/pwdb/SerializableDatabase.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/database/src/main/java/org/linguafranca/pwdb/SerializableDatabase.java b/database/src/main/java/org/linguafranca/pwdb/SerializableDatabase.java index b61bedd9..74e7a961 100644 --- a/database/src/main/java/org/linguafranca/pwdb/SerializableDatabase.java +++ b/database/src/main/java/org/linguafranca/pwdb/SerializableDatabase.java @@ -25,15 +25,15 @@ /** * This interface allows for serialization and deserialization of databases, which is * theoretically decoupled from the actual format they use for serialization. - *

        + *

        * Databases instantiate themselves from a stream and serialize to a stream, * and need to be able to encrypt and decrypt data (e.g. Protected fields in KDBX format). - *

        + *

        * KDBX V3 databases contain a header hash (i.e. a hash of the contents of * some portion of the {@link StreamFormat} they have been loaded from or saved to). * Which means that databases must support the setting of this value after the header * has been written on save, and reading the value after load to allow for integrity checking. - *

        + *

        * KDBX V4 databases have their attachments in the header so databases need to support setting * and getting of attachments for serialization */ From 4cf865a985f39788deb26481527c691b9faebaa5 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 21 Aug 2023 17:34:26 +0100 Subject: [PATCH 134/228] [maven-release-plugin] prepare release KeePassJava2-2.2 --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 4 ++-- simple/pom.xml | 2 +- test/pom.xml | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index 161d9e82..b042c41b 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2 ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index 2c4bb41d..81664e1c 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index b3175154..adfccb7c 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index 9c349724..9352f75c 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 90d1eedf..80d6d85b 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index 44af7a25..7af226cb 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index 49295390..e8fce462 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index d82912ce..6d360434 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.2.1-SNAPSHOT + 2.2 database test @@ -50,7 +50,7 @@ scm:git:git://github.com/jorabin/KeePassJava2.git scm:git:git@github.com:jorabin/KeePassJava2.git https://github.com/jorabin/KeePassJava2 - HEAD + KeePassJava2-2.2 diff --git a/simple/pom.xml b/simple/pom.xml index 7ce047c2..54e11631 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index b9ba156d..22df3951 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2 4.0.0 From 1b9a83cee3de75fec9960d325b76718773ba8ca2 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 21 Aug 2023 17:47:46 +0100 Subject: [PATCH 135/228] Revert to 2.2-SNAPSHOT --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 2 +- simple/pom.xml | 2 +- test/pom.xml | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index b042c41b..a7f91c5f 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2-SNAPSHOT ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index 81664e1c..bbe7b516 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index adfccb7c..9b425739 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index 9352f75c..0344029f 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 80d6d85b..6f4ad86d 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index 7af226cb..d0735d6e 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index e8fce462..7c289a88 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index 6d360434..27132de2 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.2 + 2.2-SNAPSHOT database test diff --git a/simple/pom.xml b/simple/pom.xml index 54e11631..504a9155 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index 22df3951..f695f755 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2-SNAPSHOT 4.0.0 From 86660602003444f4cb0a34526506d1eded9196b2 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 21 Aug 2023 17:49:11 +0100 Subject: [PATCH 136/228] [maven-release-plugin] prepare release KeePassJava2-2.2 --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 2 +- simple/pom.xml | 2 +- test/pom.xml | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index a7f91c5f..b042c41b 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index bbe7b516..81664e1c 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index 9b425739..adfccb7c 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index 0344029f..9352f75c 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 6f4ad86d..80d6d85b 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index d0735d6e..7af226cb 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index 7c289a88..e8fce462 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index 27132de2..6d360434 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.2-SNAPSHOT + 2.2 database test diff --git a/simple/pom.xml b/simple/pom.xml index 504a9155..54e11631 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index f695f755..22df3951 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 4.0.0 From 7df8458325c8d1ed66ee899747398a9af025cb40 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 21 Aug 2023 17:49:15 +0100 Subject: [PATCH 137/228] [maven-release-plugin] prepare for next development iteration --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 2 +- simple/pom.xml | 2 +- test/pom.xml | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index b042c41b..161d9e82 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index 81664e1c..2c4bb41d 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index adfccb7c..b3175154 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index 9352f75c..9c349724 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 80d6d85b..90d1eedf 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index 7af226cb..44af7a25 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index e8fce462..49295390 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index 6d360434..b338f679 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.2 + 2.2.1-SNAPSHOT database test diff --git a/simple/pom.xml b/simple/pom.xml index 54e11631..7ce047c2 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index 22df3951..b9ba156d 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT 4.0.0 From c157737147be4e4b7599604ace0b1de63c04a27d Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 21 Aug 2023 17:58:49 +0100 Subject: [PATCH 138/228] revert version again --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 5 ++++- simple/pom.xml | 2 +- test/pom.xml | 2 +- 10 files changed, 13 insertions(+), 10 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index 161d9e82..a7f91c5f 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2-SNAPSHOT ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index 2c4bb41d..bbe7b516 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index b3175154..9b425739 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index 9c349724..0344029f 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 90d1eedf..6f4ad86d 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index 44af7a25..d0735d6e 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index 49295390..7c289a88 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index b338f679..f2455108 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.2.1-SNAPSHOT + 2.2-SNAPSHOT database test @@ -277,6 +277,9 @@ jar + + none + diff --git a/simple/pom.xml b/simple/pom.xml index 7ce047c2..504a9155 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index b9ba156d..f695f755 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2-SNAPSHOT 4.0.0 From 719929c006b0e01337898ddda74e2e371fc48690 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 21 Aug 2023 18:00:24 +0100 Subject: [PATCH 139/228] [maven-release-plugin] prepare release KeepassJava2-2.2 --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 4 ++-- simple/pom.xml | 2 +- test/pom.xml | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index a7f91c5f..b042c41b 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index bbe7b516..81664e1c 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index 9b425739..adfccb7c 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index 0344029f..9352f75c 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 6f4ad86d..80d6d85b 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index d0735d6e..7af226cb 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index 7c289a88..e8fce462 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index f2455108..f5a565c9 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.2-SNAPSHOT + 2.2 database test @@ -50,7 +50,7 @@ scm:git:git://github.com/jorabin/KeePassJava2.git scm:git:git@github.com:jorabin/KeePassJava2.git https://github.com/jorabin/KeePassJava2 - KeePassJava2-2.2 + KeepassJava2-2.2 diff --git a/simple/pom.xml b/simple/pom.xml index 504a9155..54e11631 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index f695f755..22df3951 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2 4.0.0 From 893c2179aa61c7956929b83539d8c50a686b66a8 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 21 Aug 2023 18:00:27 +0100 Subject: [PATCH 140/228] [maven-release-plugin] prepare for next development iteration --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 4 ++-- simple/pom.xml | 2 +- test/pom.xml | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index b042c41b..161d9e82 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index 81664e1c..2c4bb41d 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index adfccb7c..b3175154 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index 9352f75c..9c349724 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 80d6d85b..90d1eedf 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index 7af226cb..44af7a25 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index e8fce462..49295390 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index f5a565c9..415ce361 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.2 + 2.2.1-SNAPSHOT database test @@ -50,7 +50,7 @@ scm:git:git://github.com/jorabin/KeePassJava2.git scm:git:git@github.com:jorabin/KeePassJava2.git https://github.com/jorabin/KeePassJava2 - KeepassJava2-2.2 + KeePassJava2-2.2 diff --git a/simple/pom.xml b/simple/pom.xml index 54e11631..7ce047c2 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index 22df3951..b9ba156d 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2 + 2.2.1-SNAPSHOT 4.0.0 From 13d5c2e5edb38f0cf73028cb523395a7f1f7429e Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 21 Aug 2023 18:16:13 +0100 Subject: [PATCH 141/228] Update to release 2.2.1 --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 4 ++-- simple/pom.xml | 2 +- test/pom.xml | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index 161d9e82..11d8bda0 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2.1 ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index 2c4bb41d..5289607e 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2.1 ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index b3175154..8c2a3c6d 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2.1 ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index 9c349724..686ba445 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2.1 ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 90d1eedf..b2cdc605 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2.1 ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index 44af7a25..f6c04a61 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2.1 ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index 49295390..fa9a909c 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2.1 ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index 415ce361..ea8287a2 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.2.1-SNAPSHOT + 2.2.1 database test @@ -251,7 +251,7 @@ ossrh https://oss.sonatype.org/ - false + true diff --git a/simple/pom.xml b/simple/pom.xml index 7ce047c2..e1944e04 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2.1 ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index b9ba156d..7989c7bc 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1-SNAPSHOT + 2.2.1 4.0.0 From f42cabf13183ba429d103f51ce426941f33589e2 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 21 Aug 2023 18:39:04 +0100 Subject: [PATCH 142/228] Version 2.2.1 Released, documents updated --- CHANGELOG.md | 2 +- readme.md | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eccb587..efddb174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.com) and [Semantic Versioning](http://semver.org/spec/v2.0.0.html) -##[2.2-Snapshot] +##[2.2.1] ### Added diff --git a/readme.md b/readme.md index e79bcfc8..41ff8fd8 100644 --- a/readme.md +++ b/readme.md @@ -41,7 +41,7 @@ It is licensed under the Apache 2 License and is currently usable. After a a period of neglect, the project is (May 2023) back in development. -The current code is version 2.2-SNAPSHOT. This is on the main branch. See [Build from Source](#build-from-source) +The current code is version 2.2.2-SNAPSHOT. This is on the main branch. See [Build from Source](#build-from-source) Key updates: - Java 8 (dependencies no longer support Java 7) @@ -54,21 +54,21 @@ See the [changelog](CHANGELOG.md) for more details. ### Release -The composite POM for the last release (2.1), Java 7 compatible, is +The composite POM for the last release (2.2.1), Java 8 compatible, is org.linguafranca.pwdb KeePassJava2 - 2.1.4 + 2.2.1 at Maven Central. Note that the artifactId has become Camel Case from release 2.1.x onwards. ### Snapshot -Snapshot build at [Sonatype](https://oss.sonatype.org/content/repositories/snapshots/org/linguafranca/pwdb/): +Snapshot builds are erratically available at [Sonatype](https://oss.sonatype.org/content/repositories/snapshots/org/linguafranca/pwdb/): org.linguafranca.pwdb KeePassJava2 - 2.2-SNAPSHOT + 2.2.2-SNAPSHOT with appropriate `` entry, like: From 0cb98d59a5beb68027b4bc36bb74f538ee9ef837 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 21 Aug 2023 19:02:04 +0100 Subject: [PATCH 143/228] Update to 2.2.2-SNAPSHOT --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 6 +++--- readme.md | 2 +- simple/pom.xml | 2 +- test/pom.xml | 2 +- 11 files changed, 13 insertions(+), 13 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index 11d8bda0..e44b48ad 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1 + 2.2.2-SNAPSHOT ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index 5289607e..67b6015d 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1 + 2.2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index 8c2a3c6d..f3ccfe79 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1 + 2.2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index 686ba445..bc807577 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1 + 2.2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index b2cdc605..fbe8e56a 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1 + 2.2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index f6c04a61..611a3231 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1 + 2.2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index fa9a909c..c642ea8d 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1 + 2.2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index ea8287a2..8fafb40e 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.2.1 + 2.2.2-SNAPSHOT database test @@ -178,12 +178,12 @@ com.google.guava guava - 31.1-android + 32.1.2-android org.bouncycastle bcpkix-jdk18on - 1.73 + 1.74 org.slf4j diff --git a/readme.md b/readme.md index 41ff8fd8..556fc8e8 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent) [![javadoc](https://javadoc.io/badge2/org.linguafranca.pwdb/KeePassJava2/javadoc.svg)](https://javadoc.io/doc/org.linguafranca.pwdb/KeePassJava2) -![alt text](https://badgen.net/badge/Build/2.2-SNAPSHOT/blue?icon=github) +![alt text](https://badgen.net/badge/Build/2.2.2-SNAPSHOT/blue?icon=github) master [![CircleCI](https://dl.circleci.com/status-badge/img/gh/jorabin/KeePassJava2/tree/master.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/jorabin/KeePassJava2/tree/master) develop [![CircleCI](https://dl.circleci.com/status-badge/img/gh/jorabin/KeePassJava2/tree/develop.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/jorabin/KeePassJava2/tree/develop) diff --git a/simple/pom.xml b/simple/pom.xml index e1944e04..4fbe068d 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1 + 2.2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index 7989c7bc..47961c4c 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.1 + 2.2.2-SNAPSHOT 4.0.0 From 4de1c6a495e9d0a18451ac1573c4a87dedb8306b Mon Sep 17 00:00:00 2001 From: Giuseppe Valente Date: Mon, 21 Aug 2023 23:55:53 +0200 Subject: [PATCH 144/228] Pull request review --- .gitignore | 1 + .vscode/launch.json | 42 - .vscode/settings.json | 4 - all/pom.xml | 7 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- .../linguafranca/pwdb/kdbx/OpenDbExample.java | 12 + jackson/pom.xml | 60 +- .../pwdb/kdbx/jackson/JacksonEntry.java | 1 - .../pwdb/kdbx/jackson/JacksonGroup.java | 2 - .../pwdb/kdbx/jackson/JacksonHistory.java | 1 - .../jackson/JacksonSerializableDatabase.java | 8 - .../pwdb/kdbx/jackson/model/EntryClasses.java | 14 +- .../pwdb/kdbx/jackson/model/KeePassFile.java | 6 - .../pwdb/kdbx/jackson/model/Times.java | 1 - jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 802 ++++++------------ readme.md | 9 + simple/pom.xml | 2 +- test/pom.xml | 2 +- 23 files changed, 330 insertions(+), 656 deletions(-) delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index e3020a05..86ee18b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ testOutput build +.vscode/* *.versionsBackup */target #settings.gradle diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 7d448b88..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "java", - "name": "Current File", - "request": "launch", - "mainClass": "${file}" - }, - { - "type": "java", - "name": "App", - "request": "launch", - "mainClass": "org.linguafranca.pwdb.kdbx.jackson.App", - "projectName": "KeePassJava2-jackson" - }, - { - "type": "java", - "name": "ChooseFile", - "request": "launch", - "mainClass": "org.linguafranca.pwdb.kdbx.ChooseFile", - "projectName": "example" - }, - { - "type": "java", - "name": "OpenDbExample", - "request": "launch", - "mainClass": "org.linguafranca.pwdb.kdbx.OpenDbExample", - "projectName": "example" - }, - { - "type": "java", - "name": "HexViewer", - "request": "launch", - "mainClass": "org.linguafranca.util.HexViewer", - "projectName": "test" - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 0518b0a9..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "java.compile.nullAnalysis.mode": "automatic", - "maven.view": "flat" -} \ No newline at end of file diff --git a/all/pom.xml b/all/pom.xml index a7f91c5f..892f7056 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2.2-SNAPSHOT ../pom.xml @@ -50,5 +50,10 @@ KeePassJava2-simple ${project.version} + + org.linguafranca.pwdb + KeePassJava2-jackson + ${project.version} + \ No newline at end of file diff --git a/database/pom.xml b/database/pom.xml index bbe7b516..67b6015d 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index 9b425739..f3ccfe79 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index 0344029f..bc807577 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java index 8f2b31f9..8af6c6f3 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java @@ -3,6 +3,7 @@ import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.Visitor; import org.linguafranca.pwdb.kdbx.dom.DomDatabaseWrapper; +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; import org.linguafranca.pwdb.kdbx.jaxb.JaxbDatabase; import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; @@ -45,6 +46,13 @@ public Database load(KdbxCreds creds, InputStream inputStream) throws Exception } } + private static class JacksonDbLoader implements DbLoader { + @Override + public Database load(KdbxCreds creds, InputStream inputStream) throws Exception { + return JacksonDatabase.load(creds, inputStream); + } + } + public static void testDb (DbLoader loader, String label, int loads, int iterations) throws Exception { KdbxCreds creds = new KdbxCreds("123".getBytes()); long start = System.currentTimeMillis(); @@ -66,6 +74,7 @@ public static void main(String[] args) throws Exception { testDb(new SimpleDbLoader(), "Simple", 5, 20); testDb(new JaxbDbLoader(), "Jaxb", 5, 20); testDb(new DomDbLoader(), "Dom", 5, 20); + testDb(new JacksonDbLoader(), "Jackson", 5, 20); printStream.println("Sleeping"); System.gc(); @@ -74,6 +83,7 @@ public static void main(String[] args) throws Exception { testDb(new SimpleDbLoader(), "Simple", 5, 20); testDb(new JaxbDbLoader(), "Jaxb", 5, 20); testDb(new DomDbLoader(), "Dom", 5, 20); + testDb(new JacksonDbLoader(), "Jackson", 5, 20); printStream.println("Sleeping"); System.gc(); @@ -82,6 +92,7 @@ public static void main(String[] args) throws Exception { testDb(new SimpleDbLoader(), "Simple", 10, 1); testDb(new JaxbDbLoader(), "Jaxb", 10, 1); testDb(new DomDbLoader(), "Dom", 10, 1); + testDb(new JacksonDbLoader(), "Jackson", 10, 1); printStream.println("Sleeping"); System.gc(); @@ -90,5 +101,6 @@ public static void main(String[] args) throws Exception { testDb(new SimpleDbLoader(), "Simple", 1, 50); testDb(new JaxbDbLoader(), "Jaxb", 1, 50); testDb(new DomDbLoader(), "Dom", 1, 50); + testDb(new JacksonDbLoader(), "Jackson", 1, 50); } } diff --git a/jackson/pom.xml b/jackson/pom.xml index 1215bf7e..f6c2027b 100644 --- a/jackson/pom.xml +++ b/jackson/pom.xml @@ -14,13 +14,11 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - - + KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2.2-SNAPSHOT ../pom.xml 4.0.0 @@ -54,54 +52,22 @@ junit junit - 4.11 test - - + - - - maven-clean-plugin - 3.1.0 - - - - maven-resources-plugin - 3.0.2 - - - maven-compiler-plugin - 3.8.0 - - - maven-surefire-plugin - 2.22.1 - - - maven-jar-plugin - 3.0.2 - - - maven-install-plugin - 2.5.2 - - - maven-deploy-plugin - 2.8.2 - - - - maven-site-plugin - 3.7.1 - - - maven-project-info-reports-plugin - 3.0.0 - + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + ${java.version} + src/generated/java + + - diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java index 797f8fe1..c3110e0e 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java @@ -59,7 +59,6 @@ "binary", "autoType", "history", - //"customData" }) diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java index f9fcc322..c7a9dc43 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java @@ -36,7 +36,6 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -// @JacksonXmlRootElement(localName = "Group") @JsonPropertyOrder({ "uuid", "name", @@ -51,7 +50,6 @@ "lastTopVisibleEntry", "entry", "group", -//"customData" }) @JsonIgnoreProperties(ignoreUnknown = true) public class JacksonGroup diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonHistory.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonHistory.java index dd9d6e97..05ba3537 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonHistory.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonHistory.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -// @JacksonXmlRootElement(localName = "History") public class JacksonHistory { @JacksonXmlProperty(localName = "Entry") /** Workaround jackson **/ diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java index 912eb662..d9dd388a 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java @@ -99,14 +99,6 @@ public void save(OutputStream outputStream) throws IOException { sw.writeEndDocument(); sw.close(); - //sw.writeStartElement("root"); - - - //sw.writeComment("Some insightful commentary here"); - //sw.writeEndElement(); - // sw.writeEndDocument(); - - } catch(Exception e) { throw new IllegalStateException(e); } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java index e8ae12eb..553203de 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java @@ -57,7 +57,6 @@ public static String getBinaryContent(BinaryProperty property) { return property == null || property.value == null ? null : property.value.ref; } - //@JacksonXmlRootElement(localName = "AutoType") public static class AutoType { @JacksonXmlProperty(localName = "Enabled") @JsonDeserialize(converter = StringToBooleanConverter.class) @@ -84,7 +83,6 @@ public static class Association { } } - //@JacksonXmlRootElement(localName = "String") public static class StringProperty { public StringProperty() { @@ -117,7 +115,7 @@ public void setKey(String key) { public void setValue(Value value) { this.value = value; } - // @JsonSerialize(using = ValueBooleanSerializer.class) + public static class Value { public Value() { @@ -134,19 +132,13 @@ public Value(String text, Boolean _protected) { } @JacksonXmlProperty(localName = "ProtectInMemory", isAttribute = true) - //@JsonDeserialize(converter = StringToBooleanConverter.class) - //@JsonSerialize(converter = BooleanToStringConverter.class) protected Boolean protectInMemory; - @JacksonXmlProperty(localName = "Protected", isAttribute = true) - //@JsonDeserialize(converter = StringToBooleanConverter.class) - //@JsonSerialize(converter = BooleanToStringConverter.class) + @JacksonXmlProperty(localName = "Protected", isAttribute = true) Boolean _protected; @JacksonXmlProperty(localName = "kpj2-ProtectOnOutput", isAttribute = true) - //@JsonDeserialize(converter = StringToBooleanConverter.class) - //@JsonSerialize(converter = BooleanToStringConverter.class) Boolean protectOnOutput; @JacksonXmlText @@ -188,7 +180,6 @@ public void setProtectInMemory(Boolean protectInMemory) { } } - // @JacksonXmlRootElement(localName = "Binary") public static class BinaryProperty { @JacksonXmlProperty(localName = "Key") @@ -223,7 +214,6 @@ public static class History { @JacksonXmlProperty(localName = "Entry") /** Workaround jackson **/ @JacksonXmlElementWrapper(useWrapping = false) - //@JacksonXmlElementWrapper(localName = "JacksonEntry", useWrapping = false) private List list; public History() { diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java index 2e87b62b..6c7691bd 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java @@ -52,7 +52,6 @@ public void createBinaries() { meta.binaries = new ArrayList<>(); } - // @JacksonXmlRootElement(localName = "Root") @JsonPropertyOrder({ "group", "deletedObject" @@ -70,7 +69,6 @@ public Root() { } - // @JacksonXmlRootElement(localName = "Meta") @JsonPropertyOrder({ "generator", "headerHash", @@ -97,7 +95,6 @@ public Root() { "historyMaxItems", "historyMaxSize", "binaries", - //"customData" }) public static class Meta { @@ -272,7 +269,6 @@ public Binaries() { } } - // @JacksonXmlRootElement(localName = "Binary") public static class Binary { public Binary() { @@ -314,7 +310,6 @@ public void setCompressed(boolean compressed) { } } - // @JacksonXmlRootElement(localName = "Icon") public static class Icon { public Icon() { } @@ -342,7 +337,6 @@ public String getName() { } } - // @JacksonXmlRootElement(localName = "CustomData") public static class CustomData { public CustomData() { diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/Times.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/Times.java index 80477e07..0e1f8ba2 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/Times.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/Times.java @@ -25,7 +25,6 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -// @JacksonXmlRootElement(localName = "Times") public class Times { @JacksonXmlProperty(localName = "LastModificationTime") @JsonDeserialize(converter = StringToDateConverter.class) diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 6f4ad86d..fbe8e56a 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index d0735d6e..611a3231 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index 7c289a88..c642ea8d 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index 496fb3aa..5595bb85 100644 --- a/pom.xml +++ b/pom.xml @@ -1,4 +1,4 @@ - + - - 4.0.0 - - org.linguafranca.pwdb - - KeePassJava2-parent - - 2.2-SNAPSHOT - - - - database - - test - - kdb - - kdbx - - dom - - simple - - jaxb - - all - - example - - jackson - - - - pom - - KeePassJava2 - - A Java 8 API for databases compatible with the renowned KeePass password safe for Windows - - https://github.com/jorabin/KeePassJava2 - - - - - - Apache License, Version 2.0 - - http://www.apache.org/licenses/LICENSE-2.0.txt - - repo - - - - - - - - scm:git:git://github.com/jorabin/KeePassJava2.git - - scm:git:git@github.com:jorabin/KeePassJava2.git - + --> + + + 4.0.0 + + org.linguafranca.pwdb + KeePassJava2-parent + 2.2.2-SNAPSHOT + + database + test + kdb + kdbx + dom + simple + jaxb + jackson + all + example + + pom + + KeePassJava2 + A Java 8 API for databases compatible with the renowned KeePass password safe for Windows https://github.com/jorabin/KeePassJava2 - - HEAD - - - - - - - - Jo Rabin - - Europe/London - - https://www.linkedin.com/in/jorabin - - - - Creator - - - - - - - - - - 3.0.4 - - - - - - UTF-8 - - 1.8 - - 3.11.0 - - 3.0.0 - - 1.6.13 - - 3.2.1 - - 3.5.0 - - 3.0.1 - - 3.0.0 - - 3.0.2 - - - - - - - - - - - - org.apache.maven.plugins - - maven-surefire-plugin - - ${maven-surefire-plugin.version} - - - - false - - - - - - ${inhibitConsoleOutput} - - - - - - - - ${maven.multiModuleProjectDirectory}/logback.xml - - - - - - - - - - listener - - org.linguafranca.util.MavenRunListener - - - - - - - - - - - - org.apache.maven.plugins - - maven-jar-plugin - - ${maven-jar-plugin.version} - - - - - - - - ${project.groupId}.${project.artifactId} - - - - - - - - - - - - - - - - - - org.apache.maven.plugins - - maven-compiler-plugin - - ${maven-compiler-plugin.version} - - - - ${java.version} - - ${java.version} - - - - - - - - org.apache.maven.plugins - - maven-release-plugin - - ${maven-release-plugin.version} - - - - true - - false - - releaseToMaven,signJar - - deploy - - - - - - - - org.apache.maven.plugins - - maven-dependency-plugin - - 3.0.2 - - - - - - - - - - - - junit - - junit - - - - - - org.slf4j - - slf4j-api - - - - - - ch.qos.logback - - logback-classic - - - - - - - - - - - - org.jetbrains - - annotations - - 24.0.1 - - - - - - - - com.google.guava - - guava - - 31.1-android - - - - - - org.bouncycastle - - bcpkix-jdk18on - - 1.73 - - - - - - org.slf4j - - slf4j-api - - 2.0.5 - - - - - - - - ch.qos.logback - - logback-classic - - 1.3.7 - - - - - - commons-codec - - commons-codec - - 1.15 - - - - - - junit - - junit - - 4.13.2 - - - - - - - - - - - - - - inhibitConsoleOutput - - - - true - - - - - - - - signJar - - - - - - - - org.apache.maven.plugins - - maven-gpg-plugin - - ${maven-gpg-plugin.version} - - - - - - sign-artifacts - - verify - - - - sign - - - - - - - - - - - - - - - - - - releaseToMaven - - - + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + scm:git:git://github.com/jorabin/KeePassJava2.git + scm:git:git@github.com:jorabin/KeePassJava2.git + https://github.com/jorabin/KeePassJava2 + HEAD + + + + + Jo Rabin + Europe/London + https://www.linkedin.com/in/jorabin + + Creator + + + + + + 3.0.4 + + + + UTF-8 + 1.8 + + 3.11.0 + 3.0.0 + 1.6.13 + 3.2.1 + 3.5.0 + 3.0.1 + 3.0.0 + 3.0.2 + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + false + + + ${inhibitConsoleOutput} + + + + ${maven.multiModuleProjectDirectory}/logback.xml + + + + + listener + org.linguafranca.util.MavenRunListener + + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + ${project.groupId}.${project.artifactId} + + + + + + + - - - - org.sonatype.plugins - - nexus-staging-maven-plugin - - ${nexus-staging-maven-plugin.version} - - true - - - - ossrh - - https://oss.sonatype.org/ - - false - - - - - - - - org.apache.maven.plugins - - maven-source-plugin - - ${maven-source-plugin.version} - - - - - - attach-sources - - - - jar-no-fork - - - - - - - - - - - - org.apache.maven.plugins - - maven-javadoc-plugin - - ${maven-javadoc-plugin.version} - - - - - - attach-javadocs - - - - jar - - - - - - - - - + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-release-plugin + ${maven-release-plugin.version} + + true + false + releaseToMaven,signJar + deploy + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.0.2 + - - - - - - - - - - - - ossrh - - https://oss.sonatype.org/content/repositories/snapshots - - - - - + + + + + junit + junit + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + + + + + org.jetbrains + annotations + 24.0.1 + + + + com.google.guava + guava + 31.1-android + + + org.bouncycastle + bcpkix-jdk18on + 1.73 + + + org.slf4j + slf4j-api + 2.0.5 + + + + ch.qos.logback + logback-classic + 1.3.7 + + + commons-codec + commons-codec + 1.15 + + + junit + junit + 4.13.2 + + + + + + + + inhibitConsoleOutput + + true + + + + signJar + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + sign-artifacts + verify + + sign + + + + + + + + + releaseToMaven + + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${nexus-staging-maven-plugin.version} + true + + ossrh + https://oss.sonatype.org/ + false + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + attach-javadocs + + jar + + + + + + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + diff --git a/readme.md b/readme.md index e79bcfc8..8049a421 100644 --- a/readme.md +++ b/readme.md @@ -111,6 +111,9 @@ or Database database = DomDatabaseWrapper.load(credentials, inputStream) +or + Database database = JacksonDatabase.load(credentials, inputStream) + Different implementations have varying characteristics, primarily speed. The table below illustrates timings for the file `test1.kdbx` (in the test module resources - @@ -121,14 +124,17 @@ as assessed by [this test](https://github.com/jorabin/KeePassJava2/blob/master/e Simple 5 loads 20 iterations 257 millis Jaxb 5 loads 20 iterations 326 millis Dom 5 loads 20 iterations 758 millis + Jackson 5 loads 20 iterations 374 millis Simple 10 loads 1 iterations 340 millis Jaxb 10 loads 1 iterations 552 millis Dom 10 loads 1 iterations 175 millis + Jackson 10 loads 1 iterations 343 millis Simple 1 loads 50 iterations 28 millis Jaxb 1 loads 50 iterations 47 millis Dom 1 loads 50 iterations 251 millis + Jackson 1 loads 50 iterations 34 millis Load time is dominant in this example for JAXB and Simple, database traversal for the DOM implementation. @@ -197,6 +203,9 @@ The Simple XML implementation additionally depends on: - [Simple XML Serialisation Framework](http://simple.sourceforge.net/) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0)). - [Faster XML Aalto](https://github.com/FasterXML/aalto-xml) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0.txt)). +The Jackson implementation depends on: +- [Faster XML Jackson](https://github.com/FasterXML/jackson) + For Java 11 and later Jaxb implementation depends on explicit inclusion [no longer provided by JDK](https://docs.oracle.com/en/java/javase/11/migrate/index.html#JSMIG-GUID-F640FA9D-FB66-4D85-AD2B-D931174C09A3) of: - [JAXB](https://javaee.github.io/jaxb-v2/) diff --git a/simple/pom.xml b/simple/pom.xml index 504a9155..4fbe068d 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2.2-SNAPSHOT ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index f695f755..47961c4c 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2-SNAPSHOT + 2.2.2-SNAPSHOT 4.0.0 From 7325c7a4f356b5bcef47be1059b0407e608a9a77 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 22 Aug 2023 14:14:29 +0100 Subject: [PATCH 145/228] Tidy POM --- pom.xml | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index d30b0ef8..f01f78b5 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt + https://www.apache.org/licenses/LICENSE-2.0.txt repo @@ -51,7 +51,7 @@ scm:git:git://github.com/jorabin/KeePassJava2.git scm:git:git@github.com:jorabin/KeePassJava2.git https://github.com/jorabin/KeePassJava2 - KeePassJava2-2.2 + KeePassJava2-2.2.2 @@ -65,10 +65,6 @@ - - 3.0.4 - - UTF-8 1.8 @@ -81,6 +77,8 @@ 3.0.1 3.0.0 3.0.2 + + false @@ -141,7 +139,7 @@ true false - releaseToMaven,signJar + releaseToMaven deploy @@ -220,7 +218,7 @@ - signJar + releaseToMaven @@ -237,13 +235,6 @@ - - - - - releaseToMaven - - org.sonatype.plugins nexus-staging-maven-plugin @@ -279,7 +270,7 @@ jar - none + html, syntax From 292db7f46d14c779d9a554fd27c6941793da35fe Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 22 Aug 2023 14:14:57 +0100 Subject: [PATCH 146/228] Javadoc Nits --- .../linguafranca/pwdb/kdb/KdbSerializer.java | 10 +++++----- .../org/linguafranca/pwdb/kdbx/KdbxHeader.java | 6 +++--- .../linguafranca/pwdb/kdbx/KdbxSerializer.java | 17 +++++++++-------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java index d4256f87..468fb921 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java @@ -31,16 +31,16 @@ /** * This class provides support for reading a KDB stream and constructing an in memory database. - *

        + *

        * A KDB stream consists of: *

          *
        1. 16 bits of file signature
        2. *
        3. An unencrypted header containing encryption and other information such as the count of groups and entries
        4. - *
        5. In an Encrypted form:
        6. + *
        7. In an Encrypted form: *
            *
          1. Repeated serialised form of all Groups
          2. *
          3. Repeated serialised form of all Entries
          4. - *
          + *
        * * * @author jo @@ -291,11 +291,11 @@ private static void deserializeEntry(KdbDatabase database, DataInput dataInput) /** * Figure out who the parent of this group is. - *

        + *

        * Groups are serialised in a depth first traversal * so any group's parent is the nearest parent group * with a level of one less in the hierarchy. - *

        + *

        * Since the database tree is built progressively the group passed in the "lastGroup" parameter has * already been knitted into the hierarchy and so either this group is a sub group of that group or is a subgroup * of the nearest ancestor to the last group that has a level less than the group we are reading. diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java index d6026b38..ecfdb585 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java @@ -37,12 +37,12 @@ /** * This class represents the header portion of a KeePass KDBX file or stream. The header is received in * plain text and describes the encryption and compression of the remainder of the file. - *

        + *

        * In V4 the header became Outer Header and Inner Header. The class stores the configuration * contents of both and binary attachments. - *

        + *

        * It is a factory for encryption and decryption streams. It provides for verification of its own serialization. - *

        + *

        * While KDBX streams are Little-Endian, data is passed to and from this class in standard Java byte order. */ @SuppressWarnings("WeakerAccess") diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java index 0e36f05a..98f36eab 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java @@ -39,37 +39,38 @@ /** * This class provides static methods for the encryption and decryption of Keepass KDBX V3 and V4 files. - *

        + *

        * KDBX files are little-endian and consist of the following: *

          *
        1. An unencrypted portion
        2. - *
            + *
            1. *
            2. 8 bytes Magic number
            3. *
            4. 4 bytes version
            5. - *
            6. A header containing details of the encryption of the remainder of the file
            7. + *
            8. A header containing details of the encryption of the remainder of the file: *

              The header fields are encoded using a TLV style. The Type is an enumeration encoded in 1 byte. * The length is encoded in 4 bytes (V3: 2 bytes) and the value according to the length denoted. The sequence is * terminated by a zero type with 0 length. {@link #readOuterHeader}

              *

              In V4 there follows a 32 byte SHA-256 hash of the file so far

              *

              In V4 there follows a 32 byte HMAC-256 hash of the file so far

              *

              {@link KdbxHeader} details the fields of the header.

              - *
            + *
          1. + *
          *
        3. In V3 the remainder of the file is encrypted as follows:
        4. - *
            + *
            1. *
            2. A sequence of bytes contained in the header. If they don't match, decryption has not worked.
            3. *
            4. A payload serialized in Hashed Block format, see e.g. {@link HashedBlockInputStream} for details of this.
            5. *
            6. The content of this payload may be GZIP compressed.
            7. *
            8. The content is now a character stream, which is expected to be * XML representing a KeePass Database. Assumed UTF-8 encoding.
            9. - *
            + *
          *
        5. In V4 the remainder of the file is encoded as HMacBlocks:
        6. - *
            + *
            1. *
            2. A sequence of blocks encoded using Hmac Blocks see {@link HmacBlockInputStream}
            3. *
            4. Those blocks contain an encrypted input stream.
            5. *
            6. The encrypted input stream optionally contains a Gzipped input stream.
            7. *
            8. The content is now a character stream, which is an inner header {@link #readInnerHeader} * followed by XML representing a KeePass Database. Assumed UTF-8 encoding.
            9. - *
            + *
          *
        *

        The methods in this class provide support for serializing and deserializing plain text payload content * to and from the above encrypted format. From 4590548594c41b5ad7ea6f72aed133131fbb0999 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 22 Aug 2023 14:56:56 +0100 Subject: [PATCH 147/228] Updating documentation --- .gitignore | 3 ++- CHANGELOG.md | 8 +++++++- ModuleStructure.svg | 4 +++- readme.md | 23 ++++++++++++++++++----- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 86ee18b6..3ee52c53 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ testOutput build .vscode/* *.versionsBackup -*/target +**/target + #settings.gradle #gradlew #gradlew.bat diff --git a/CHANGELOG.md b/CHANGELOG.md index efddb174..c87262d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.com) and [Semantic Versioning](http://semver.org/spec/v2.0.0.html) +##[2.2.2] + +### Added + +- implementation of database using Jackson + ##[2.2.1] ### Added @@ -44,7 +50,7 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.co - Various functionality for searching databases - Recycle bin functionality - An experimental implementation of [keepasshttp](https://github.com/pfn/keepasshttp/) see -[the readme](http/readme.md) for warnings, limitations, etc. about this. +[the readme](./readme.md) for warnings, limitations, etc. about this. ## [2.1.2] 2018-01-20 diff --git a/ModuleStructure.svg b/ModuleStructure.svg index 7b271e9e..9579dffe 100644 --- a/ModuleStructure.svg +++ b/ModuleStructure.svg @@ -1,2 +1,4 @@ + + -
        all
        all
        simple
        simple
        jaxb
        jaxb
        dom
        dom
        kdb
        kdb
        kdbx
        kdbx
        database
        database
        Test
        Test
        example
        example
        Other, future
        Other, future
        KDBX Implementations
        KDBX Implementations
        KDB Implementation
        KDB Implementation
        ???
        ???
        Implementatiions
        of the 
        org.linguafranca.pwdb.Database Interface
        [Not supported by viewer]
        \ No newline at end of file +
        all
        all
        simple
        simple
        jaxb
        jaxb
        dom
        dom
        kdb
        kdb
        kdbx
        kdbx
        database
        database
        Test
        Test
        example
        example
        Other, future
        Other,...
        KDBX Implementations
        KDBX I...
        KDB Implementation
        KDB Im...
        ???
        ???
        Implementatiions
        of the 
        org.linguafranca.pwdb.Database Interface
        Implem...
        jackson
        jackson
        Text is not SVG - cannot display
        \ No newline at end of file diff --git a/readme.md b/readme.md index 35c15338..d7a208b0 100644 --- a/readme.md +++ b/readme.md @@ -39,11 +39,11 @@ It is licensed under the Apache 2 License and is currently usable. ## Current Status -After a a period of neglect, the project is (May 2023) back in development. +After a period of neglect, the project is (May 2023) back in development. The current code is version 2.2.2-SNAPSHOT. This is on the main branch. See [Build from Source](#build-from-source) -Key updates: +Key updates relative to 2.1: - Java 8 (dependencies no longer support Java 7) - Updated dependencies to remove known vulnerabilities - File format version 4 support - with Argon2 @@ -112,6 +112,7 @@ or Database database = DomDatabaseWrapper.load(credentials, inputStream) or + Database database = JacksonDatabase.load(credentials, inputStream) Different implementations have varying characteristics, primarily speed. @@ -204,6 +205,7 @@ The Simple XML implementation additionally depends on: - [Faster XML Aalto](https://github.com/FasterXML/aalto-xml) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0.txt)). The Jackson implementation depends on: + - [Faster XML Jackson](https://github.com/FasterXML/jackson) For Java 11 and later Jaxb implementation depends on explicit inclusion [no longer provided by JDK](https://docs.oracle.com/en/java/javase/11/migrate/index.html#JSMIG-GUID-F640FA9D-FB66-4D85-AD2B-D931174C09A3) of: @@ -267,6 +269,12 @@ bindings might be useful for building other interfaces.

    Javadocs A DOM based implementation of KDBX. Being DOM based it is rather slow, but messes less with existing content than the other two implementations. Known to work on Android.
    domKeePassJava2-jacksonJavadocsA Jackson based implementation of KDBX. Intended to replace the Simple XML implementation. Simple XML seems no +longer to be maintained.
    @@ -274,12 +282,12 @@ Why are there so many implementations for KDBX? Well, the DOM implementation cam load and save stuff that the implementation doesn't specifically know about. But it is very slow. Then came the JAXB implementation, but -belatedly it seems that Android support is in question. So latterly the Simple implementation. That's probably enough -KDBX implementations. +belatedly it seems that Android support is in question. So latterly the Simple implementation. That was probably enough +KDBX implementations, however, the Simple XML library seems no longer to be maintained, and along comes the Jackson Implementation. ### Gradle -If you prefer Gradle the automatic conversion `gradle init` converts the POM successfully, however you will +If you prefer Gradle the automatic conversion `gradle init` has been known to convert the POM successfully, however you will need to add something like [gradle-source-sets.txt](jaxb/gradle-source-sets.txt) to the `build.gradle` for the JAXB module, so that the generated sources get compiled correctly. @@ -292,6 +300,11 @@ In [this file](./CHANGELOG.md). Many thanks to Pavel Ivanov [@ivanovpv](https://github.com/ivanovpv) for his help with Android and Gradle compatibility issues. +Thanks to Giuseppe Valente [@giusvale-dev](https://github.com/giusvale-dev) for +the contribution of the Jackson module. + +Thanks to other contributors and raisers of issues. + ## License Copyright (c) 2023 Jo Rabin From 4095cebedf9fb83ba80dff0400109b2e2b5d6e08 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 22 Aug 2023 15:05:32 +0100 Subject: [PATCH 148/228] removing unnecessary config for compiler in jackson POM --- jackson/pom.xml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/jackson/pom.xml b/jackson/pom.xml index f6c2027b..6f721427 100644 --- a/jackson/pom.xml +++ b/jackson/pom.xml @@ -55,19 +55,4 @@ test
    - - - - - org.apache.maven.plugins - maven-compiler-plugin - ${maven-compiler-plugin.version} - - ${java.version} - ${java.version} - src/generated/java - - - - From eadb0b0607824bce17513185222fb879577c719c Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Wed, 23 Aug 2023 17:00:54 +0100 Subject: [PATCH 149/228] Tweaks to POMs --- .../org/linguafranca/pwdb/package-info.java | 9 +++++++++ jackson/pom.xml | 20 +++++-------------- pom.xml | 4 ++-- 3 files changed, 16 insertions(+), 17 deletions(-) create mode 100644 all/src/main/java/org/linguafranca/pwdb/package-info.java diff --git a/all/src/main/java/org/linguafranca/pwdb/package-info.java b/all/src/main/java/org/linguafranca/pwdb/package-info.java new file mode 100644 index 00000000..a544df96 --- /dev/null +++ b/all/src/main/java/org/linguafranca/pwdb/package-info.java @@ -0,0 +1,9 @@ +/** + * This module provides a simple import of the database implementations of KeePass, "all" in the diagram below. + *

    + * + *

    + * @see Module Structure + * in the readme at GitHub for a discussion of the project modules and links to JavaDocs. + */ +package org.linguafranca.pwdb; \ No newline at end of file diff --git a/jackson/pom.xml b/jackson/pom.xml index 6f721427..1e63cf51 100644 --- a/jackson/pom.xml +++ b/jackson/pom.xml @@ -33,26 +33,16 @@ KeePassJava2-kdbx ${project.version} + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + 2.15.0 + org.linguafranca.pwdb test ${project.version} test - - com.fasterxml.jackson.dataformat - jackson-dataformat-xml - 2.15.0 - - - com.fasterxml.woodstox - woodstox-core - 6.5.0 - - - junit - junit - test - diff --git a/pom.xml b/pom.xml index f01f78b5..b6f03e3a 100644 --- a/pom.xml +++ b/pom.xml @@ -146,7 +146,7 @@ org.apache.maven.plugins maven-dependency-plugin - 3.0.2 + 3.6.0 @@ -182,7 +182,7 @@ org.bouncycastle bcpkix-jdk18on - 1.74 + 1.76 org.slf4j From bc38b85e1f0e613104e2ae93223cbd4032d4296b Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Thu, 24 Aug 2023 15:11:36 +0100 Subject: [PATCH 150/228] Correct name --- .../converter/{ValueDeserialized.java => ValueDeserializer.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/{ValueDeserialized.java => ValueDeserializer.java} (100%) diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserialized.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java similarity index 100% rename from jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserialized.java rename to jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java From e2d74647fa279d5e17d9a1d599c2433718fbe772 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Thu, 24 Aug 2023 15:13:00 +0100 Subject: [PATCH 151/228] Correct corruption of database by ValueSerializer #50 --- .../jackson/converter/ValueSerializer.java | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java index 640f5db8..4d0207d2 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java @@ -30,15 +30,7 @@ public class ValueSerializer extends StdSerializer{ - private StreamEncryptor encryptor; - - public ValueSerializer() { - super(ValueSerializer.class, false); - } - - public ValueSerializer(Class v) { - super(v); - } + private final StreamEncryptor encryptor; public ValueSerializer(StreamEncryptor encryptor) { super(ValueSerializer.class, false); @@ -52,8 +44,9 @@ public void serialize(Value value, JsonGenerator gen, SerializerProvider provide final ToXmlGenerator xmlGenerator = (ToXmlGenerator) gen; xmlGenerator.writeStartObject(); + String stringToWrite = value.getText(); //We need to encrypt and convert to base64 every protected element - if(value.getProtectOnOutput()) { + if (value.getProtectOnOutput()) { xmlGenerator.setNextIsAttribute(true); gen.writeStringField("Protected", "True"); String plain = value.getText(); @@ -63,17 +56,12 @@ public void serialize(Value value, JsonGenerator gen, SerializerProvider provide //Cipher byte[] encrypted = encryptor.encrypt(plain.getBytes()); //Convert to base64 - String base64 = new String(Base64.encodeBase64(encrypted)); - - //Destroy from memory the plain value - plain = null; - value.setText(base64); - + stringToWrite = new String(Base64.encodeBase64(encrypted)); } xmlGenerator.setNextIsAttribute(false); xmlGenerator.setNextIsUnwrapped(true); - xmlGenerator.writeStringField("text",value.getText()); + xmlGenerator.writeStringField("text", stringToWrite); gen.writeEndObject(); From 7cb44889c7ab76d1a6a5b318bc24f05236224d1b Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Thu, 24 Aug 2023 15:14:36 +0100 Subject: [PATCH 152/228] Correct protection of default fields #51 and tidy --- .../jackson/JacksonSerializableDatabase.java | 84 ++++++++++++++++--- 1 file changed, 72 insertions(+), 12 deletions(-) diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java index d9dd388a..2ecf4f58 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java @@ -19,14 +19,19 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamWriter; +import com.fasterxml.jackson.databind.MapperFeature; +import org.jetbrains.annotations.NotNull; +import org.linguafranca.pwdb.Entry; import org.linguafranca.pwdb.SerializableDatabase; import org.linguafranca.pwdb.kdbx.Helpers; -import org.linguafranca.pwdb.kdbx.jackson.converter.ValueDeserialized; +import org.linguafranca.pwdb.kdbx.jackson.converter.ValueDeserializer; import org.linguafranca.pwdb.kdbx.jackson.converter.ValueSerializer; import org.linguafranca.pwdb.kdbx.jackson.model.EntryClasses; import org.linguafranca.pwdb.kdbx.jackson.model.KeePassFile; @@ -41,6 +46,8 @@ import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; +import static org.linguafranca.pwdb.Entry.STANDARD_PROPERTY_NAME_TITLE; + public class JacksonSerializableDatabase implements SerializableDatabase { public KeePassFile keePassFile; @@ -67,7 +74,7 @@ public JacksonSerializableDatabase(KeePassFile keePassFile) { public JacksonSerializableDatabase load(InputStream inputStream) throws IOException { XmlMapper mapper = new XmlMapper(); SimpleModule module = new SimpleModule(); - module.addDeserializer(EntryClasses.StringProperty.Value.class, new ValueDeserialized(encryptor)); + module.addDeserializer(EntryClasses.StringProperty.Value.class, new ValueDeserializer(encryptor)); mapper.registerModule(module); keePassFile = mapper.readValue(inputStream, KeePassFile.class); return this; @@ -76,35 +83,88 @@ public JacksonSerializableDatabase load(InputStream inputStream) throws IOExcept @Override public void save(OutputStream outputStream) throws IOException { - + prepareForSave(keePassFile.root.group); try { - - XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newFactory(); - XmlMapper mapper = new XmlMapper(); + SimpleModule module = new SimpleModule(); module.addSerializer(EntryClasses.StringProperty.Value.class, new ValueSerializer(encryptor)); + // disable auto-detection, only use annotated values + XmlMapper mapper = XmlMapper.builder() + .disable(MapperFeature.AUTO_DETECT_CREATORS, + MapperFeature.AUTO_DETECT_FIELDS, + MapperFeature.AUTO_DETECT_GETTERS, + MapperFeature.AUTO_DETECT_SETTERS, + MapperFeature.AUTO_DETECT_IS_GETTERS) + .build(); mapper.registerModule(module); mapper.enable(ToXmlGenerator.Feature.WRITE_XML_DECLARATION); mapper.enable(SerializationFeature.INDENT_OUTPUT); + mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + + // set the serializer to Woodstox + System.setProperty("javax.xml.stream.XMLOutputFactory", "com.ctc.wstx.stax.WstxOutputFactory"); + XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newFactory(); xmlOutputFactory.setProperty(WstxOutputProperties.P_USE_DOUBLE_QUOTES_IN_XML_DECL, true); xmlOutputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, false); xmlOutputFactory.setProperty(WstxInputProperties.P_RETURN_NULL_FOR_DEFAULT_NAMESPACE, true); OutputStreamWriter osw = new OutputStreamWriter(outputStream); XMLStreamWriter sw = xmlOutputFactory.createXMLStreamWriter(osw); - sw.setPrefix("xml", "http://www.w3.org/XML/1998/namespace"); + try { + sw.setPrefix("xml", "http://www.w3.org/XML/1998/namespace"); - mapper.writeValue(sw, keePassFile); - - sw.writeEndDocument(); - sw.close(); + mapper.writeValue(sw, keePassFile); + + sw.writeEndDocument(); + } finally { + sw.close(); + osw.close(); + } } catch(Exception e) { throw new IllegalStateException(e); } } - + /** + * Create a list of names of properties that should be encrypted by default + */ + @NotNull + private List getToEncrypt() { + final List toEncrypt = new ArrayList<>(); + for (String propertyName: Entry.STANDARD_PROPERTY_NAMES) { + if (keePassFile.meta.memoryProtection.shouldProtect(propertyName)) { + toEncrypt.add(propertyName); + } + } + return toEncrypt; + } + + /** + * Utility to mark fields that need to be encrypted and vice versa + * + * @param parent the group to start from + */ + private static void prepareForSave(JacksonGroup parent){ + for (JacksonGroup group: parent.groups) { + prepareForSave(group); + } + for (JacksonEntry entry: parent.entries) { + for (EntryClasses.StringProperty property : entry.string) { + boolean shouldProtect = parent.database.shouldProtect(property.getKey()); + property.getValue().setProtectOnOutput(shouldProtect || property.getValue().getProtectOnOutput()); + } + if (Objects.nonNull(entry.history)) { + for (JacksonEntry entry2 : entry.history.getEntry()) { + for (EntryClasses.StringProperty property : entry2.string) { + boolean shouldProtect = parent.database.shouldProtect(property.getKey()); + property.getValue().setProtectOnOutput(shouldProtect || property.getValue().getProtectOnOutput()); + } + } + } + } + } + @Override public byte[] getHeaderHash() { return keePassFile.meta.headerHash; From 4a007f3e6cd14a2d826e719210c77f7a2de8cf7f Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Thu, 24 Aug 2023 15:16:41 +0100 Subject: [PATCH 153/228] Various tidies --- .../pwdb/kdbx/validation/Issue33Test.java | 7 +++++++ jackson/pom.xml | 5 +++++ .../linguafranca/pwdb/kdbx/jackson/JacksonEntry.java | 6 +++--- .../linguafranca/pwdb/kdbx/jackson/JacksonGroup.java | 11 ++++++----- .../pwdb/kdbx/jackson/JacksonHistory.java | 4 ++-- .../kdbx/jackson/converter/ValueDeserializer.java | 12 ++++++------ .../pwdb/kdbx/jackson/model/EntryClasses.java | 2 +- .../pwdb/kdbx/jaxb/JaxbSerializableDatabase.java | 2 +- 8 files changed, 31 insertions(+), 18 deletions(-) diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue33Test.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue33Test.java index 09486d82..322ee10a 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue33Test.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue33Test.java @@ -10,6 +10,7 @@ import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.kdbx.Util; import org.linguafranca.pwdb.kdbx.dom.DomDatabaseWrapper; +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; import org.linguafranca.pwdb.kdbx.jaxb.JaxbDatabase; import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; @@ -59,4 +60,10 @@ public void testSimpleDatabase() throws IOException { SimpleDatabase database = SimpleDatabase.load(CREDENTIALS, inputStream); database.save(new StreamFormat.None(), new Credentials.None(), Files.newOutputStream(Paths.get(TEST_OUTPUT_DIR, "Issue33Simple.xml"))); } + + @Test + public void testJacksonDatabase() throws IOException { + JacksonDatabase database = JacksonDatabase.load(CREDENTIALS, inputStream); + database.save(new StreamFormat.None(), new Credentials.None(), Files.newOutputStream(Paths.get(TEST_OUTPUT_DIR, "Issue33Jackson.xml"))); + } } diff --git a/jackson/pom.xml b/jackson/pom.xml index 1e63cf51..0f1d8021 100644 --- a/jackson/pom.xml +++ b/jackson/pom.xml @@ -38,6 +38,11 @@ jackson-dataformat-xml 2.15.0 + + com.fasterxml.woodstox + woodstox-core + 6.5.0 + org.linguafranca.pwdb test diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java index c3110e0e..92bee42e 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java @@ -94,18 +94,18 @@ public class JacksonEntry extends AbstractEntry string; - @JacksonXmlProperty(localName = "Binary") /** Workaround jackson **/ + @JacksonXmlProperty(localName = "Binary") /* Workaround jackson */ @JacksonXmlElementWrapper(useWrapping = false) protected List binary; @JacksonXmlProperty(localName = "AutoType") protected AutoType autoType; - @JacksonXmlProperty(localName = "History") /** Workaround jackson **/ + @JacksonXmlProperty(localName = "History") /* Workaround jackson */ protected JacksonHistory history; @JsonIgnore diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java index c7a9dc43..436d66d5 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java @@ -21,7 +21,9 @@ import java.util.List; import java.util.UUID; +import com.fasterxml.jackson.annotation.JsonAutoDetect; import org.jetbrains.annotations.NotNull; +import org.linguafranca.pwdb.base.AbstractGroup; import org.linguafranca.pwdb.kdbx.jackson.converter.StringToBooleanConverter; import org.linguafranca.pwdb.kdbx.jackson.converter.UUIDToBase64Converter; import org.linguafranca.pwdb.kdbx.jackson.converter.Base64ToUUIDConverter; @@ -51,9 +53,8 @@ "entry", "group", }) -@JsonIgnoreProperties(ignoreUnknown = true) -public class JacksonGroup - extends org.linguafranca.pwdb.base.AbstractGroup { +@JsonIgnoreProperties(ignoreUnknown=true) +public class JacksonGroup extends AbstractGroup { @JacksonXmlProperty(localName = "UUID") @JsonDeserialize(converter = Base64ToUUIDConverter.class) @@ -100,12 +101,12 @@ public class JacksonGroup @JsonSerialize(converter = UUIDToBase64Converter.class) protected UUID lastTopVisibleEntry; - @JacksonXmlProperty(localName = "Entry") /** Workaround jackson **/ + @JacksonXmlProperty(localName = "Entry") /* Workaround jackson */ @JacksonXmlElementWrapper(useWrapping = false) protected List entries; - @JacksonXmlProperty(localName = "Group") /** Workaround jackson **/ + @JacksonXmlProperty(localName = "Group") /* Workaround jackson */ @JacksonXmlElementWrapper(useWrapping = false) protected List groups; diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonHistory.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonHistory.java index 05ba3537..c9ea238c 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonHistory.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonHistory.java @@ -23,12 +23,12 @@ public class JacksonHistory { - @JacksonXmlProperty(localName = "Entry") /** Workaround jackson **/ + @JacksonXmlProperty(localName = "Entry") /* Workaround jackson */ @JacksonXmlElementWrapper(useWrapping = false) private List entry; public JacksonHistory() { - entry = new ArrayList(); + entry = new ArrayList<>(); } /** diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java index e448589e..8f7fda9b 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java @@ -29,20 +29,20 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -public class ValueDeserialized extends StdDeserializer { +public class ValueDeserializer extends StdDeserializer { private StreamEncryptor encryptor; - public ValueDeserialized() { - super(ValueDeserialized.class); + public ValueDeserializer() { + super(ValueDeserializer.class); } - public ValueDeserialized(Class v) { + public ValueDeserializer(Class v) { super(v); } - public ValueDeserialized(StreamEncryptor encryptor) { - super(ValueDeserialized.class); + public ValueDeserializer(StreamEncryptor encryptor) { + super(ValueDeserializer.class); this.encryptor = encryptor; } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java index 553203de..05b9c441 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java @@ -212,7 +212,7 @@ public void setRef(String ref) { public static class History { - @JacksonXmlProperty(localName = "Entry") /** Workaround jackson **/ + @JacksonXmlProperty(localName = "Entry") /* Workaround jackson */ @JacksonXmlElementWrapper(useWrapping = false) private List list; diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java index b9a0f034..188af88b 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java @@ -186,7 +186,7 @@ public void afterMarshal(Object source) { } /** - * Create a list of names of peroperties that should be encrypted by default + * Create a list of names of properties that should be encrypted by default */ @NotNull private List getToEncrypt() { From 324da07071b172d0e3d1b476acd03a3fe96d0c65 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 25 Aug 2023 16:16:20 +0100 Subject: [PATCH 154/228] Upate ChangeLog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c87262d1..a791c2db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,13 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.com) and [Semantic Versioning](http://semver.org/spec/v2.0.0.html) -##[2.2.2] +## [2.2.2-SNAPSHOT] ### Added - implementation of database using Jackson -##[2.2.1] +## [2.2.1] 2023-08-21 ### Added From 1bce2e1482fd7998af628255d251b953d3f52738 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 25 Aug 2023 18:01:40 +0100 Subject: [PATCH 155/228] Tidy --- .../pwdb/kdbx/jackson/JacksonDatabase.java | 21 ++++----- .../pwdb/kdbx/jackson/JacksonGroup.java | 38 ++++++---------- .../jackson/JacksonSerializableDatabase.java | 43 ++++++++----------- 3 files changed, 41 insertions(+), 61 deletions(-) diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java index 1c15344c..ef99e038 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java @@ -16,14 +16,6 @@ package org.linguafranca.pwdb.kdbx.jackson; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Date; -import java.util.List; -import java.util.Objects; -import java.util.UUID; - import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.StreamConfiguration; import org.linguafranca.pwdb.StreamFormat; @@ -32,17 +24,22 @@ import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import org.linguafranca.pwdb.kdbx.jackson.model.KeePassFile; -import com.fasterxml.jackson.core.exc.StreamReadException; -import com.fasterxml.jackson.databind.DatabindException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.UUID; -import static org.linguafranca.pwdb.kdbx.jackson.JacksonSerializableDatabase.createEmptyDatabase;; +import static org.linguafranca.pwdb.kdbx.jackson.JacksonSerializableDatabase.createEmptyDatabase; public class JacksonDatabase extends AbstractDatabase { KeePassFile keePassFile; StreamFormat streamFormat; - public JacksonDatabase() throws StreamReadException, DatabindException, IOException { + public JacksonDatabase() throws IOException { this(createEmptyDatabase(), null); } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java index 436d66d5..f71ffb3d 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java @@ -16,20 +16,6 @@ package org.linguafranca.pwdb.kdbx.jackson; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.UUID; - -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import org.jetbrains.annotations.NotNull; -import org.linguafranca.pwdb.base.AbstractGroup; -import org.linguafranca.pwdb.kdbx.jackson.converter.StringToBooleanConverter; -import org.linguafranca.pwdb.kdbx.jackson.converter.UUIDToBase64Converter; -import org.linguafranca.pwdb.kdbx.jackson.converter.Base64ToUUIDConverter; -import org.linguafranca.pwdb.kdbx.jackson.converter.BooleanToStringConverter; -import org.linguafranca.pwdb.kdbx.jackson.model.Times; - import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonPropertyOrder; @@ -37,6 +23,18 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import org.jetbrains.annotations.NotNull; +import org.linguafranca.pwdb.base.AbstractGroup; +import org.linguafranca.pwdb.kdbx.jackson.converter.Base64ToUUIDConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.BooleanToStringConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.StringToBooleanConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.UUIDToBase64Converter; +import org.linguafranca.pwdb.kdbx.jackson.model.Times; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; @JsonPropertyOrder({ "uuid", @@ -165,11 +163,7 @@ public void setParent(JacksonGroup group) { @Override public List getGroups() { - List result = new ArrayList<>(); - for (JacksonGroup aGroup : groups) { - result.add(aGroup); - } - return result; + return new ArrayList<>(groups); } @Override @@ -208,11 +202,7 @@ public JacksonGroup removeGroup(JacksonGroup group) { @Override public List getEntries() { - List result = new ArrayList<>(); - for (JacksonEntry entry : this.entries) { - result.add(entry); - } - return result; + return new ArrayList<>(this.entries); } @Override diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java index 2ecf4f58..5aedec57 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java @@ -15,18 +15,13 @@ */ package org.linguafranca.pwdb.kdbx.jackson; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -import javax.xml.stream.XMLOutputFactory; -import javax.xml.stream.XMLStreamWriter; - +import com.ctc.wstx.api.WstxInputProperties; +import com.ctc.wstx.api.WstxOutputProperties; import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; import org.jetbrains.annotations.NotNull; import org.linguafranca.pwdb.Entry; import org.linguafranca.pwdb.SerializableDatabase; @@ -37,29 +32,27 @@ import org.linguafranca.pwdb.kdbx.jackson.model.KeePassFile; import org.linguafranca.pwdb.security.StreamEncryptor; -import com.ctc.wstx.api.WstxInputProperties; -import com.ctc.wstx.api.WstxOutputProperties; -import com.fasterxml.jackson.core.exc.StreamReadException; -import com.fasterxml.jackson.databind.DatabindException; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.dataformat.xml.XmlMapper; -import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; - -import static org.linguafranca.pwdb.Entry.STANDARD_PROPERTY_NAME_TITLE; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; public class JacksonSerializableDatabase implements SerializableDatabase { public KeePassFile keePassFile; private StreamEncryptor encryptor; - public static KeePassFile createEmptyDatabase() throws StreamReadException, DatabindException, IOException { + public static KeePassFile createEmptyDatabase() throws IOException { InputStream inputStream = JacksonSerializableDatabase.class.getClassLoader() .getResourceAsStream("base.kdbx.xml"); XmlMapper mapper = new XmlMapper(); - KeePassFile res = mapper.readValue(inputStream, KeePassFile.class); - return res; + return mapper.readValue(inputStream, KeePassFile.class); } @@ -82,7 +75,7 @@ public JacksonSerializableDatabase load(InputStream inputStream) throws IOExcept @Override - public void save(OutputStream outputStream) throws IOException { + public void save(OutputStream outputStream) { prepareForSave(keePassFile.root.group); try { From 01dc088dfcd46e205c2c282c9c61ea0d35c8918c Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 25 Aug 2023 18:06:31 +0100 Subject: [PATCH 156/228] tidy --- .../pwdb/kdbx/jackson/converter/Base64ToByteConverter.java | 3 +-- .../pwdb/kdbx/jackson/converter/Base64ToUUIDConverter.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToByteConverter.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToByteConverter.java index feb67339..66eb416c 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToByteConverter.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToByteConverter.java @@ -24,7 +24,6 @@ public class Base64ToByteConverter extends StdConverter { @Override public byte[] convert(String value) { - byte[] decode = Base64.getDecoder().decode(value); - return decode; + return Base64.getDecoder().decode(value); } } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToUUIDConverter.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToUUIDConverter.java index c77c267f..b2b07d29 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToUUIDConverter.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToUUIDConverter.java @@ -26,7 +26,7 @@ public class Base64ToUUIDConverter extends StdConverter { @Override public UUID convert(String value) { - if(value == null || value.equals("")) { + if(value == null || value.isEmpty()) { return UUID.randomUUID(); } return Helpers.uuidFromBase64(value); From d952a9873fc45c1729cf5524267b75c785d82078 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 25 Aug 2023 18:14:36 +0100 Subject: [PATCH 157/228] tidy --- .../pwdb/kdbx/jackson/converter/UUIDToBase64Converter.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/UUIDToBase64Converter.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/UUIDToBase64Converter.java index 3106bc2e..3564185e 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/UUIDToBase64Converter.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/UUIDToBase64Converter.java @@ -25,11 +25,6 @@ public class UUIDToBase64Converter extends StdConverter { @Override public String convert(UUID value) { - - if(value != null) { - return Helpers.base64FromUuid(value); - } - return new String(); + return Helpers.base64FromUuid(value); } - } From 99ce6ec18cd48f2724b413ceeaf534d3e2a91269 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 25 Aug 2023 19:21:52 +0100 Subject: [PATCH 158/228] More tudying --- .../linguafranca/pwdb/kdbx/ChooseFile.java | 6 ++-- .../converter/StringToDateConverter.java | 19 +++++------- .../jackson/converter/ValueDeserializer.java | 30 +++++-------------- .../jackson/converter/ValueSerializer.java | 9 ++---- 4 files changed, 21 insertions(+), 43 deletions(-) diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java index 80cbf8ae..e0f4f266 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java @@ -67,14 +67,12 @@ public String getDescription() { return; } logger.info("Opening {}", fc.getSelectedFile().getPath()); -/* Util.listXml(fc.getSelectedFile().getName(), new KdbxCreds(s.getBytes()), new PrintWriter(outputStream)); -*/ - try (InputStream is = Files.newInputStream(Paths.get(fc.getSelectedFile().getPath()))) { +/* try (InputStream is = Files.newInputStream(Paths.get(fc.getSelectedFile().getPath()))) { HexViewer.list(is); - } + }*/ } } } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/StringToDateConverter.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/StringToDateConverter.java index d334a5bb..c215f90e 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/StringToDateConverter.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/StringToDateConverter.java @@ -16,6 +16,7 @@ package org.linguafranca.pwdb.kdbx.jackson.converter; import java.util.Date; +import java.util.Objects; import org.linguafranca.pwdb.kdbx.Helpers; @@ -25,17 +26,13 @@ public class StringToDateConverter extends StdConverter { @Override public Date convert(String value) { - Date result = null; - if(value != null) { - if(value.equals("${creationDate}")) { - result = new Date(); - } - try { - result = Helpers.toDate(value); - } catch(Exception e) { - result = new Date(); - } + // TODO: It would really be better if we could inhibit deserialize date elements that are not present + if (Objects.isNull(value) || value.isEmpty()) { + return null; } - return result; + if(value.equals("${creationDate}")) { + return new Date(); + } + return Helpers.toDate(value); } } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java index 8f7fda9b..926aaa8a 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java @@ -31,15 +31,7 @@ public class ValueDeserializer extends StdDeserializer { - private StreamEncryptor encryptor; - - public ValueDeserializer() { - super(ValueDeserializer.class); - } - - public ValueDeserializer(Class v) { - super(v); - } + private final StreamEncryptor encryptor; public ValueDeserializer(StreamEncryptor encryptor) { super(ValueDeserializer.class); @@ -47,8 +39,7 @@ public ValueDeserializer(StreamEncryptor encryptor) { } @Override - public EntryClasses.StringProperty.Value deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException { - + public EntryClasses.StringProperty.Value deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { JsonNode node = p.getCodec().readTree(p); @@ -56,14 +47,12 @@ public EntryClasses.StringProperty.Value deserialize(JsonParser p, Deserializati if(node.isTextual()) { result.setText(node.textValue()); - } else if(node.isObject()) { - + return result; + } + if(node.isObject()) { + // TODO not clear what is happening here, looks like it's not exactly correct //We need to decrypt all Protected values - if(node.has("Protected")) { - - //Check if Protected=True - Boolean nodeEncrypted = Helpers.toBoolean(node.get("Protected").asText()); - if(nodeEncrypted) { + if(node.has("Protected") && Boolean.TRUE.equals(Helpers.toBoolean(node.get("Protected").asText()))) { if(node.has("")) { String cipherText = node.get("").asText(); if(cipherText != null && !cipherText.isEmpty()) { @@ -75,7 +64,6 @@ public EntryClasses.StringProperty.Value deserialize(JsonParser p, Deserializati result.setProtectOnOutput(true); } } - } } else { //If an element is not marked us Protected we need to copy the value as is if(node.has("ProtectInMemory")) { @@ -88,8 +76,6 @@ public EntryClasses.StringProperty.Value deserialize(JsonParser p, Deserializati } } } - return result; } - -} +} \ No newline at end of file diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java index 4d0207d2..1e020390 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java @@ -48,13 +48,13 @@ public void serialize(Value value, JsonGenerator gen, SerializerProvider provide //We need to encrypt and convert to base64 every protected element if (value.getProtectOnOutput()) { xmlGenerator.setNextIsAttribute(true); - gen.writeStringField("Protected", "True"); + xmlGenerator.writeStringField("Protected", "True"); String plain = value.getText(); if(plain == null) { plain = ""; } //Cipher - byte[] encrypted = encryptor.encrypt(plain.getBytes()); + byte[] encrypted = encryptor.encrypt(plain.getBytes()); //Convert to base64 stringToWrite = new String(Base64.encodeBase64(encrypted)); } @@ -62,9 +62,6 @@ public void serialize(Value value, JsonGenerator gen, SerializerProvider provide xmlGenerator.setNextIsAttribute(false); xmlGenerator.setNextIsUnwrapped(true); xmlGenerator.writeStringField("text", stringToWrite); - - gen.writeEndObject(); - + xmlGenerator.writeEndObject(); } - } From cfe806de1f1eb565b02a345423e6ea1c659c076c Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 25 Aug 2023 19:29:52 +0100 Subject: [PATCH 159/228] Tidying --- .../jackson/converter/ValueDeserializer.java | 50 +++++++++---------- .../jackson/converter/ValueSerializer.java | 19 ++++--- 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java index 926aaa8a..91fc3330 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java @@ -15,19 +15,17 @@ */ package org.linguafranca.pwdb.kdbx.jackson.converter; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import org.apache.commons.codec.binary.Base64; import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.jackson.model.EntryClasses; import org.linguafranca.pwdb.security.StreamEncryptor; -import com.fasterxml.jackson.core.JacksonException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import java.io.IOException; +import java.nio.charset.StandardCharsets; public class ValueDeserializer extends StdDeserializer { @@ -40,40 +38,40 @@ public ValueDeserializer(StreamEncryptor encryptor) { @Override public EntryClasses.StringProperty.Value deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - + JsonNode node = p.getCodec().readTree(p); - + EntryClasses.StringProperty.Value result = new EntryClasses.StringProperty.Value(); - if(node.isTextual()) { + if (node.isTextual()) { result.setText(node.textValue()); return result; } - if(node.isObject()) { + if (node.isObject()) { // TODO not clear what is happening here, looks like it's not exactly correct //We need to decrypt all Protected values - if(node.has("Protected") && Boolean.TRUE.equals(Helpers.toBoolean(node.get("Protected").asText()))) { - if(node.has("")) { - String cipherText = node.get("").asText(); - if(cipherText != null && !cipherText.isEmpty()) { + if (node.has("Protected") && Boolean.TRUE.equals(Helpers.toBoolean(node.get("Protected").asText()))) { + if (node.has("")) { + String cipherText = node.get("").asText(); + if (cipherText != null && !cipherText.isEmpty()) { - //Decode to byte the Base64 text - byte[] encrypted = Base64.decodeBase64(cipherText.getBytes()); - String decrypted = new String(encryptor.decrypt(encrypted), StandardCharsets.UTF_8); - result.setText(decrypted); - result.setProtectOnOutput(true); - } + //Decode to byte the Base64 text + byte[] encrypted = Base64.decodeBase64(cipherText.getBytes()); + String decrypted = new String(encryptor.decrypt(encrypted), StandardCharsets.UTF_8); + result.setText(decrypted); + result.setProtectOnOutput(true); } + } } else { //If an element is not marked us Protected we need to copy the value as is - if(node.has("ProtectInMemory")) { + if (node.has("ProtectInMemory")) { Boolean protectInMemory = Helpers.toBoolean(node.get("ProtectInMemory").asText()); result.setProtectInMemory(protectInMemory); - - if(node.has("")) { + + if (node.has("")) { result.setText(node.get("").asText()); } - } + } } } return result; diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java index 1e020390..a6c8fc81 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java @@ -15,20 +15,19 @@ */ package org.linguafranca.pwdb.kdbx.jackson.converter; -import java.io.IOException; - +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; import org.apache.commons.codec.binary.Base64; import org.linguafranca.pwdb.kdbx.jackson.model.EntryClasses; import org.linguafranca.pwdb.kdbx.jackson.model.EntryClasses.StringProperty.Value; import org.linguafranca.pwdb.security.StreamEncryptor; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; +import java.io.IOException; -public class ValueSerializer extends StdSerializer{ +public class ValueSerializer extends StdSerializer { private final StreamEncryptor encryptor; @@ -40,7 +39,7 @@ public ValueSerializer(StreamEncryptor encryptor) { @Override public void serialize(Value value, JsonGenerator gen, SerializerProvider provider) throws IOException { - + final ToXmlGenerator xmlGenerator = (ToXmlGenerator) gen; xmlGenerator.writeStartObject(); @@ -50,7 +49,7 @@ public void serialize(Value value, JsonGenerator gen, SerializerProvider provide xmlGenerator.setNextIsAttribute(true); xmlGenerator.writeStringField("Protected", "True"); String plain = value.getText(); - if(plain == null) { + if (plain == null) { plain = ""; } //Cipher @@ -64,4 +63,4 @@ public void serialize(Value value, JsonGenerator gen, SerializerProvider provide xmlGenerator.writeStringField("text", stringToWrite); xmlGenerator.writeEndObject(); } -} +} \ No newline at end of file From ba2a8b5712dce6c8ad6a685ceaba65b5265de4e2 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Thu, 31 Aug 2023 11:57:51 +0100 Subject: [PATCH 160/228] initial pass at implementing protected property values --- PropertyValueProtection.md | 50 ++++++ database/pom.xml | 5 + .../java/org/linguafranca/pwdb/Database.java | 25 ++- .../java/org/linguafranca/pwdb/Entry.java | 61 ++++++- .../org/linguafranca/pwdb/PropertyValue.java | 166 ++++++++++++++++++ .../linguafranca/pwdb/base/AbstractEntry.java | 2 +- .../linguafranca/pwdb/kdb/KdbSerializer.java | 10 +- 7 files changed, 308 insertions(+), 11 deletions(-) create mode 100644 PropertyValueProtection.md create mode 100644 database/src/main/java/org/linguafranca/pwdb/PropertyValue.java diff --git a/PropertyValueProtection.md b/PropertyValueProtection.md new file mode 100644 index 00000000..a1932c58 --- /dev/null +++ b/PropertyValueProtection.md @@ -0,0 +1,50 @@ +Properties that have sensitive values are called "Protected Properties". Up to +version KeePassJava2 2.2, what is, and what is not, a protected property is determined +by the database that is loaded from file, which sets protection on a property +by property basis. + +The database also specifies that new Entries should have fields of +specified names should be protected by default, typically this is restricted to +the `password` property. Up to version 2.2 new databases followed this convention +by setting `password` as the only property protected by default. + +In the KeePass format, protected properties are encrypted in addition to the +encryption applied to the database as a whole when saved as a KeePass file. + +Up to version 2.2 of KeePassJava2 protected property values have been held as +unencrypted Strings in memory and have been accessed as Strings using the +`String getProperty(String)` and `String getPassword(String)` methods. + +From version 2.3 of KeePassJava2, the storage of property values is controlled by the +`PropertyValue` interface. Users of KeePassJava2 may set implementations +of `PropertyValue` to be used for unprotected and protected properties +so that they may have control over how those property values are stored in memory. + +The method + +`void Database.setPropertyValueStrategy(PropertyValue.Builder unprotectedPropertyBuilder, + PropertyValue.Builder protectedPropertyBuilder);` + +is used to set the strategy. By default `PropertyValue.Default` and `PropertyValue.Protected` +are set as the means of storage of values. + +New accessors for property values are provided in the `Entry` class. + +`PropertyValue getPropertyValue(String)` and `void setPropertyValue(String, PropertyValue)` +are intended as the primary means of access to property values. + +`String getPropertyValue(String)` and `void setPropertyValue(String, String)` +are now deprecated. `getPropertyValue` continues to retrieve both protected +and unprotected values as String. `setPropertyValue` stores values as either +protected or unprotected according to whether `Database.shouldProtect` returns +true or false. + +A new method `Database.setProtectByDefault` is added to control whether +properties of a particular name will be protected or not. Calling this method +after a database is loaded causes the protection of all fields of all entries +affected to be updated to use the strategy defined on the Database. + + + + + diff --git a/database/pom.xml b/database/pom.xml index 67b6015d..9d44df21 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -25,5 +25,10 @@ org.bouncycastle bcpkix-jdk18on + + io.github.novacrypto + SecureString + 2022.01.17 + \ No newline at end of file diff --git a/database/src/main/java/org/linguafranca/pwdb/Database.java b/database/src/main/java/org/linguafranca/pwdb/Database.java index eb867bdf..58394576 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Database.java +++ b/database/src/main/java/org/linguafranca/pwdb/Database.java @@ -247,12 +247,33 @@ public interface Database , G extends Group StreamFormat getStreamFormat(); /** - * Properties to encrypt + * Property to protect in memory * @param propertyName the property of interest - * @return true if it should be encrypted + * @return true if it should be protected by default */ boolean shouldProtect(String propertyName); + /** + * Property to protect in memory + * @param propertyName the property of interest + * @param protect whether to protect by default + */ + void setShouldProtect(String propertyName, boolean protect); + + /** + * Obtain a list of those properties that should be protected by default + * @return a list of property names + */ + List getShouldProtect(); + + /** + * Set the default means of storage of unprotected and protected property values + * @param unprotectedPropertyFactory a builder for unprotected properties + * @param protectedPropertyFactory a builder for protected properties + */ + void setPropertyValueStrategy(PropertyValue.Factory unprotectedPropertyFactory, + PropertyValue.Factory protectedPropertyFactory); + /** * returns true if the database supports non-standard property names */ diff --git a/database/src/main/java/org/linguafranca/pwdb/Entry.java b/database/src/main/java/org/linguafranca/pwdb/Entry.java index 4780a0ff..dc75db5a 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Entry.java +++ b/database/src/main/java/org/linguafranca/pwdb/Entry.java @@ -95,7 +95,8 @@ interface Matcher { String getPath(); /** - * Gets the value of a property. + * Gets the value of a property as a String. Use of this method is not recommended for fields with protected values. + * Use {@link #getPropertyAsChars(String)} or {@link #getPropertyValue(String)} * *

    All implementations of Entry are required to support reading and writing of * {@link #STANDARD_PROPERTY_NAMES}. @@ -106,9 +107,32 @@ interface Matcher { String getProperty(String name); /** - * Sets the value of a property. + * Gets the value of a property as a char array. * - *

    Other than the {@link #STANDARD_PROPERTY_NAMES} support for this methd is optional. + *

    All implementations of Entry are required to support reading and writing of + * {@link #STANDARD_PROPERTY_NAMES}. + * @param name the name of the property to get + * @return a value or null if the property is not known, or if setting of arbitrary properties is not supported + * @see Database#supportsNonStandardPropertyNames() + */ + char [] getPropertyAsChars(String name); + + /** + * Gets the value of a property as a PropertyValue. + * + *

    All implementations of Entry are required to support reading and writing of + * {@link #STANDARD_PROPERTY_NAMES}. + * @param name the name of the property to get + * @return a value or null if the property is not known, or if setting of arbitrary properties is not supported + * @see Database#supportsNonStandardPropertyNames() + */ + PropertyValue getPropertyValue(String name); + + /** + * Sets the value of a property. Use of this method is not recommended for fields with protected values. + * Use {@link #setProperty(String, char[])} or {@link #setProperty(String, PropertyValue)} + * + *

    Other than the {@link #STANDARD_PROPERTY_NAMES} support for this method is optional. * * @param name the name of the property to set * @param value the value to set it to @@ -118,6 +142,34 @@ interface Matcher { */ void setProperty(String name, String value); + /** + * Sets the value of a property as a property value. The method does not check whether the + * passed PropertyValue is protected relative to {@link Database#shouldProtect(String)} + * + *

    Other than the {@link #STANDARD_PROPERTY_NAMES} support for this method is optional. + * + * @param name the name of the property to set + * @param value the value to set it to + * @throws UnsupportedOperationException if the name is not one of the standard properties and + * non-standard properties are not supported + * @see Database#supportsNonStandardPropertyNames() + */ + void setProperty(String name, PropertyValue value); + + /** + * Sets the value of a property using a char array. The method checks whether the value should be + * stored as a protected value or not by reference to {@link Database#shouldProtect(String)} + * + *

    Other than the {@link #STANDARD_PROPERTY_NAMES} support for this method is optional. + * + * @param name the name of the property to set + * @param value the value to set it to + * @throws UnsupportedOperationException if the name is not one of the standard properties and + * non-standard properties are not supported + * @see Database#supportsNonStandardPropertyNames() + */ + void setProperty(String name, char [] value); + /** * Removes this non-standard property, if it exists. * @@ -222,6 +274,7 @@ interface Matcher { * * @return a password */ + @Deprecated String getPassword(); /** @@ -230,7 +283,9 @@ interface Matcher { *

    Implementations should Touch LastModifiedTime when this method is called. * * @param pass a password + * @deprecated use {@link #setProperty(String, PropertyValue)} */ + @Deprecated void setPassword(String pass); /** diff --git a/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java b/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java new file mode 100644 index 00000000..bceabeea --- /dev/null +++ b/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java @@ -0,0 +1,166 @@ +package org.linguafranca.pwdb; + +import io.github.novacrypto.SecureCharBuffer; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; + +/** + * An interface through which property values can be stored in memory to make it + * harder to access their values via a heap dump etc. + */ +public interface PropertyValue { + String getValueAsString(); + + char [] getValueAsChars(); + + byte [] getValueAsBytes(); + + boolean isProtected(); + + /** + * A builder interface for PropertyValue + */ + interface Factory { + PropertyValue of (CharSequence aCharSequence); + + PropertyValue of (char [] value); + + PropertyValue of (byte [] value); + } + + CharSequence getValue(); + + /** + * Unprotected value does not use String + */ + + class Default implements PropertyValue { + + private final CharBuffer value; + + static class Factory implements PropertyValue.Factory { + + @Override + public PropertyValue of(CharSequence aCharSequence) { + return new Default(aCharSequence); + } + + @Override + public PropertyValue of(char[] value) { + return new Default(value); + } + + @Override + public PropertyValue of(byte[] value) { + return new Default(value); + } + } + + public Default(CharSequence aString) { + this.value = CharBuffer.wrap(aString); + } + + public Default(char [] value) { + this.value = CharBuffer.wrap(value); + } + + public Default(byte [] value) { + this.value = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(value)); + } + + @Override + public String getValueAsString() { + return this.value.toString(); + } + + @Override + public CharSequence getValue() { + return this.value; + } + + @Override + public char [] getValueAsChars() { + return this.value.array(); + } + + @Override + public byte [] getValueAsBytes() { + return StandardCharsets.UTF_8.encode(value).array(); + } + + @Override + public boolean isProtected() { + return false; + } + } + + + /** + * Protected Value uses {@link SecureCharBuffer} + */ + class Protected implements PropertyValue { + private final SecureCharBuffer value; + + /** + * Builder for {@link PropertyValue.Protected} + */ + static class Factory implements PropertyValue.Factory { + + @Override + public PropertyValue of(CharSequence aString) { + return new Protected(aString); + } + + @Override + public PropertyValue of(char[] value) { + return new Protected(value); + } + + @Override + public PropertyValue of(byte[] value) { + return new Protected(value); + } + } + + public Protected(CharSequence aString) { + this.value = SecureCharBuffer.withCapacity(aString.length()); + this.value.append(aString); + } + + public Protected(char [] value) { + this.value = SecureCharBuffer.withCapacity(value.length); + this.value.append(CharBuffer.wrap(value)); + } + + public Protected(byte [] value) { + this.value = SecureCharBuffer.withCapacity(value.length); + this.value.append(StandardCharsets.UTF_8.decode(ByteBuffer.wrap(value))); + } + @Override + public String getValueAsString() { + return this.value.toStringAble().toString(); + } + + @Override + public CharSequence getValue() { + return this.value; + } + + @Override + public char [] getValueAsChars() { + return CharBuffer.wrap(value).array(); + } + + @Override + public byte [] getValueAsBytes() { + return StandardCharsets.UTF_8.encode(CharBuffer.wrap(value)).array(); + } + + @Override + public boolean isProtected() { + return true; + } + } +} diff --git a/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java b/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java index af5614fc..3cdc4e35 100644 --- a/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java +++ b/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java @@ -60,7 +60,7 @@ public boolean match(Entry.Matcher matcher) { @Override public String getPath() { - Group parent = this.getParent(); + G parent = this.getParent(); String result = ""; if (parent != null) { result = parent.getPath(); diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java index 468fb921..51e357de 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java @@ -54,7 +54,7 @@ private KdbSerializer() { } /** - * Construct a KDB database from the supplied inputstream. + * Construct a KDB database from the supplied inputStream. * * @param credentials the credentials * @param kdbHeader a header to be populated with values read from the stream @@ -297,7 +297,7 @@ private static void deserializeEntry(KdbDatabase database, DataInput dataInput) * with a level of one less in the hierarchy. *

    * Since the database tree is built progressively the group passed in the "lastGroup" parameter has - * already been knitted into the hierarchy and so either this group is a sub group of that group or is a subgroup + * already been knitted into the hierarchy and so either this group is a subgroup of that group or is a subgroup * of the nearest ancestor to the last group that has a level less than the group we are reading. * * @param lastGroup the last group we saw in the stream @@ -308,7 +308,7 @@ private static void deserializeEntry(KdbDatabase database, DataInput dataInput) private static KdbGroup computeParentGroup(KdbGroup lastGroup, int level) { // the level of the last group int lastLevel = lastGroup.computedLevel(); - // if we are one greater then we are its child + // if we are one greater, then we are its child if (level == lastLevel + 1) { return lastGroup; } @@ -326,7 +326,7 @@ private static KdbGroup computeParentGroup(KdbGroup lastGroup, int level) { } /** - * Stucture is in this format: 00YYYYYY YYYYYYMM MMDDDDDH HHHHMMMM MMSSSSSS + * Structure is in this format: 00YYYYYY YYYYYYMM MMDDDDDH HHHHMMMM MMSSSSSS * * @param buffer 5 bytes containing a packed date * @return a date constructed from the buffer @@ -364,7 +364,7 @@ public static Date unpackDate(byte[] buffer) { cal.setTimeZone(TimeZone.getTimeZone("UTC")); //noinspection MagicConstant cal.set(year, month - 1, day, hour, minute, second); - // otherwise we seems to end up with arbitrary millis + // otherwise we seem to end up with arbitrary millis cal.set(GregorianCalendar.MILLISECOND, 0); return cal.getTime(); } From d95188e986bbde4cb65b9ba8e2d2c0dc13127666 Mon Sep 17 00:00:00 2001 From: Giuseppe Valente Date: Sun, 3 Sep 2023 17:34:38 +0200 Subject: [PATCH 161/228] #41 Added support for hash check in the KeyFile (v. 2) --- .../linguafranca/pwdb/kdbx/KdbxKeyFile.java | 19 +++++++++++++++- .../pwdb/kdbx/KdbxKeyFileTest.java | 21 ++++++++++++++++++ test/src/main/resources/kdbx_hash_test.kdbx | Bin 0 -> 1870 bytes test/src/main/resources/kdbx_hash_test.keyx | 12 ++++++++++ .../resources/kdbx_hash_test_wrong_hash.keyx | 12 ++++++++++ 5 files changed, 63 insertions(+), 1 deletion(-) create mode 100755 test/src/main/resources/kdbx_hash_test.kdbx create mode 100755 test/src/main/resources/kdbx_hash_test.keyx create mode 100755 test/src/main/resources/kdbx_hash_test_wrong_hash.keyx diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java index 9d2b52e5..c855d302 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java @@ -18,6 +18,7 @@ import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Hex; +import org.linguafranca.pwdb.security.Encryption; import org.w3c.dom.Document; import javax.xml.parsers.DocumentBuilder; @@ -26,6 +27,7 @@ import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; import java.io.InputStream; +import java.security.MessageDigest; /** * Class has a static method to load a key from a KDBX XML Key File @@ -52,7 +54,22 @@ public static byte[] load(InputStream inputStream) { return null; } if (version.equals("2.0")) { - return Hex.decodeHex(data.replaceAll("\\s","")); + + byte[] hexData = Hex.decodeHex(data.replaceAll("\\s","")); + + MessageDigest md = Encryption.getSha256MessageDigestInstance(); + byte[] computedHash = md.digest(hexData); + + String hashToCheck = (String) xpath.evaluate("//KeyFile/Key/Data/@Hash", doc, XPathConstants.STRING); + byte[] verifiedHash = Hex.decodeHex(hashToCheck); + + for(int i = 0; i < verifiedHash.length; i++) { + if(computedHash[i] != verifiedHash[i]) { + return null; + } + } + return hexData; + } return Base64.decodeBase64(data.getBytes()); } catch (Exception e) { diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java index aa6b4716..1bed6fa1 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java @@ -98,4 +98,25 @@ public void testEmptyPassword() throws Exception { InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); toConsole(decryptedInputStream); } + + /** + * Test the hash in KeyFile (v2.0) + */ + @Test + public void testSignedKeyFile() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("kdbx_hash_test.kdbx"); + InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("kdbx_hash_test.keyx"); + Credentials credentials = new KdbxCreds("123".getBytes(), inputStreamKeyFile); + InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); + toConsole(decryptedInputStream); + } + + @Test(expected = RuntimeException.class) + public void testSignatureFails() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("kdbx_hash_test.kdbx"); + InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("kdbx_hash_test_wrong_hash.keyx"); + Credentials credentials = new KdbxCreds("123".getBytes(), inputStreamKeyFile); + InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); + toConsole(decryptedInputStream); + } } \ No newline at end of file diff --git a/test/src/main/resources/kdbx_hash_test.kdbx b/test/src/main/resources/kdbx_hash_test.kdbx new file mode 100755 index 0000000000000000000000000000000000000000..42ee092524719035c1ab2427312b07bd417d8b31 GIT binary patch literal 1870 zcmV-U2eJ4A*`k_f`%AR}00RI55CAd3^5(yBLr}h01tDtuTK@wC0096100bZa{K9bk zh%qk@b&|*Qb#enZbzY{H(6R!31mp__Kl8Cba3hC zi(Ej4Tr2nOlxv9w2moN}00000000LN0HJ5@*1<+*MjPUg%UjX`rU)PaB1{Z6xW> zH>i+-FGuKF(EaQTwm@reMFbK(0R_ozsuRo|vH!`~84cpldwaa=zc6(w(y#i3CFPS- zE8R|hQtIP!m1AyNIV%F&xYKWA`#|C?)#o{ZMoOO);--nM!FK9}T(iM7jA0RRH6Z-? zn!{{K4Nl;ezbyCFQ()O+OcP_Dm$1fvrqaT8zjE_~^W^;KfN z@($WVSEn!~2Z@k%yu0a4lD`7IME$j-xV@##~wWOf6u7Tg8z}!o68AqLTc=kud@t-5Y z0D+lpeDRivtzE?@cVe$7A(-B2m}AnL3OJ-`n<>*|`VYY_Pgftdegrr0{sBP(nz~gg z`zn4)%Eb@Rg0Sg8!2JJpO8Jj(lTz=Rulw<4mP4uoBRV);w_$@qmM#clUZ-jw6001A zVS#Tazp-Y(+xxFsb+?55_x5}|cmTKN#eDR-sT_*p;yUPk2AYfVr zxj<(VlG)1yxi=vjpjbW4{wFM0|6UK${Z_{6UDE>c{!n%**dS6=I||nc^%!9J!(5o_ z?5xSOIFXu*y$kD*4MHq!Xk-|EI4{8Gkc_$?vr`?*YOFQ@8qvui3Y~-Ir+e?F9VirE zV#RBm<78Q^eF@UHc{hpdD$^ZIU*7Oi>`)KcD+0{({4F2AUZ=c{sWvxw4Xzp+#_iv`Tgt{WyS$PzGd~!g=HsHZxf->O%^{y`{9a`im6DxlN}H z+8^uDzP=od3ENN|i07G{UA9gHV|f(ktTFmw*2@*>NP6M&wpr&i{k&-^_)Uh(iuc|S zaoa<%)xQiBiZ_u>4V&t!xRR9XqY)Dx9r z_c}o?GcTOIbO(fBB@WLp1RnLU(i@nE3poeB{Zp(Q-v5wldqq1`lEeEj*-ZvJ(A>?B zDT=Yw09riCztmG^5)* zpdT!tgE{da*V$9jK6RHdOzb)5!JOr<>4~ivB~LsbxSq|FjQ&m!&C#`-!rtoof5Zz* zY5ks5oZc=|dk6W;C?!ENbB91Q{mjrh%az7T)%yz=@2T`^i)ao@O4b?!=>xcm4%aJ0;O;!T8=FcNRm}+QyK~@guPsiF0FnPbhCAy{vFbHlAv%7e znBq^W2Uol5@A=j5)23qf^4A(WPEj&9ReE}ULKK2ejTrex#qXzCk!?njh1dsN zw4!GTo`u1!&~Ss5s$__4us4Nvk$=7 zD^KSbF9TB5MC^1vgUXan6psqnFt&DYnqo%b&^TQ$Xk9X5K7R + + + 2.0 + + + + 66F110CA E32995B5 EFA8E672 1E70C773 + 48F8E260 EEDD8744 93F41803 5BFDC27D + + + \ No newline at end of file diff --git a/test/src/main/resources/kdbx_hash_test_wrong_hash.keyx b/test/src/main/resources/kdbx_hash_test_wrong_hash.keyx new file mode 100755 index 00000000..5c7b37e2 --- /dev/null +++ b/test/src/main/resources/kdbx_hash_test_wrong_hash.keyx @@ -0,0 +1,12 @@ + + + + 2.0 + + + + 66F110CA E32995B5 EFA8E672 1E70C773 + 48F8E260 EEDD8744 93F41803 5BFDC27D + + + \ No newline at end of file From 7d84ced58d723e8c20764f4c30e14ce98043d81f Mon Sep 17 00:00:00 2001 From: Giuseppe Valente Date: Wed, 6 Sep 2023 00:05:14 +0200 Subject: [PATCH 162/228] #41 PR update the review code Signed-off-by: Giuseppe Valente --- .../main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java | 8 ++++---- .../java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java index c855d302..f3ad7dbd 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java @@ -28,6 +28,7 @@ import javax.xml.xpath.XPathFactory; import java.io.InputStream; import java.security.MessageDigest; +import java.util.Arrays; /** * Class has a static method to load a key from a KDBX XML Key File @@ -63,10 +64,9 @@ public static byte[] load(InputStream inputStream) { String hashToCheck = (String) xpath.evaluate("//KeyFile/Key/Data/@Hash", doc, XPathConstants.STRING); byte[] verifiedHash = Hex.decodeHex(hashToCheck); - for(int i = 0; i < verifiedHash.length; i++) { - if(computedHash[i] != verifiedHash[i]) { - return null; - } + boolean isHashVerified = Arrays.equals(Arrays.copyOf(computedHash, verifiedHash.length), verifiedHash); + if(!isHashVerified) { + throw new IllegalStateException("Hash mismatch error"); } return hexData; diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java index 1bed6fa1..4b8ea5f2 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java @@ -111,6 +111,9 @@ public void testSignedKeyFile() throws Exception { toConsole(decryptedInputStream); } + /** + * Test hash fails in KeyFile (v2.0) + */ @Test(expected = RuntimeException.class) public void testSignatureFails() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("kdbx_hash_test.kdbx"); From 1cc52f7a3dfd6a0d8da555dcc50cceb7f3686029 Mon Sep 17 00:00:00 2001 From: Giuseppe Valente Date: Wed, 6 Sep 2023 23:12:05 +0200 Subject: [PATCH 163/228] #41 closes: implemented all features --- .../org/linguafranca/pwdb/kdbx/Helpers.java | 42 +++++++++ .../linguafranca/pwdb/kdbx/KdbxKeyFile.java | 81 +++++++++++++----- .../pwdb/kdbx/KdbxKeyFileTest.java | 38 +++++++- .../main/resources/kdb_with_random_file.kdbx | Bin 0 -> 1870 bytes test/src/main/resources/kdbx_keyfile32.kdbx | Bin 0 -> 1870 bytes test/src/main/resources/kdbx_keyfile64.kdbx | Bin 0 -> 1870 bytes test/src/main/resources/keyfile32 | 1 + test/src/main/resources/keyfile64 | 1 + test/src/main/resources/random_file | Bin 0 -> 3072 bytes 9 files changed, 139 insertions(+), 24 deletions(-) create mode 100755 test/src/main/resources/kdb_with_random_file.kdbx create mode 100755 test/src/main/resources/kdbx_keyfile32.kdbx create mode 100755 test/src/main/resources/kdbx_keyfile64.kdbx create mode 100755 test/src/main/resources/keyfile32 create mode 100755 test/src/main/resources/keyfile64 create mode 100755 test/src/main/resources/random_file diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java index 35f989f8..e66a063a 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java @@ -23,6 +23,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.text.SimpleDateFormat; @@ -32,9 +33,14 @@ import java.util.Date; import java.util.TimeZone; import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + /** * The class provides helpers to marshal and unmarshal values of KDBX files */ @@ -201,4 +207,40 @@ public static byte[] toBytes(int value, ByteOrder byteOrder) { .putInt(value); return longBuffer; } + + /** + * Check if the data is an XML file. + * + * @param data the file to check + * @return true if and only if the data is an XML parsable + */ + public static boolean checkIfKeyFileIsXml(byte[] data) { + try { + InputStream is = new ByteArrayInputStream(data); + DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + documentBuilder.parse(is); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * Check if the data is a valid hex + * + * @param data the data to check + * @return true if and only if the key contained in the KeyFile is an Hex format + * @throws IllegalAccessException if the length of the data is not equal to 64 + */ + public static boolean isValidHexString(byte[] data) { + + if(data == null) { + return false; + } + + final Pattern hexPattern = Pattern.compile("\\p{XDigit}+"); + final Matcher matcher = hexPattern.matcher(new String(data)); + return matcher.matches(); + } + } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java index f3ad7dbd..ea5c7269 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java @@ -21,15 +21,21 @@ import org.linguafranca.pwdb.security.Encryption; import org.w3c.dom.Document; +import com.google.common.io.ByteStreams; + import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; + +import java.io.ByteArrayInputStream; import java.io.InputStream; import java.security.MessageDigest; import java.util.Arrays; + + /** * Class has a static method to load a key from a KDBX XML Key File * @@ -46,32 +52,61 @@ public class KdbxKeyFile { * @return the key */ public static byte[] load(InputStream inputStream) { + try { - DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - Document doc = documentBuilder.parse(inputStream); - String version = (String) xpath.evaluate("//KeyFile/Meta/Version/text()", doc, XPathConstants.STRING); - String data = (String) xpath.evaluate("//KeyFile/Key/Data/text()", doc, XPathConstants.STRING); - if (data == null) { - return null; - } - if (version.equals("2.0")) { - - byte[] hexData = Hex.decodeHex(data.replaceAll("\\s","")); - - MessageDigest md = Encryption.getSha256MessageDigestInstance(); - byte[] computedHash = md.digest(hexData); - - String hashToCheck = (String) xpath.evaluate("//KeyFile/Key/Data/@Hash", doc, XPathConstants.STRING); - byte[] verifiedHash = Hex.decodeHex(hashToCheck); - - boolean isHashVerified = Arrays.equals(Arrays.copyOf(computedHash, verifiedHash.length), verifiedHash); - if(!isHashVerified) { - throw new IllegalStateException("Hash mismatch error"); + byte[] inputBytes = ByteStreams.toByteArray(inputStream); + if (inputBytes.length == 32) { + //32 bytes KeyFile + return inputBytes; + } else if (inputBytes.length == 64) { + //64 bytes KeyFile (only Hexadecimal values) + if (Helpers.isValidHexString(inputBytes)) { + byte[] keyFile = org.bouncycastle.util.encoders.Hex.decode(inputBytes); + return keyFile; + } else { + throw new IllegalStateException("KeyFile contains not allowed characters"); + } + } else { + + //Standard KeyFile + if (Helpers.checkIfKeyFileIsXml(inputBytes)) { + + InputStream is = new ByteArrayInputStream(inputBytes); + DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document doc = documentBuilder.parse(is); + String version = (String) xpath.evaluate("//KeyFile/Meta/Version/text()", doc, + XPathConstants.STRING); + String data = (String) xpath.evaluate("//KeyFile/Key/Data/text()", doc, XPathConstants.STRING); + if (data == null) { + return null; + } + if (version.equals("2.0")) { + + byte[] hexData = Hex.decodeHex(data.replaceAll("\\s", "")); + + MessageDigest md = Encryption.getSha256MessageDigestInstance(); + byte[] computedHash = md.digest(hexData); + + String hashToCheck = (String) xpath.evaluate("//KeyFile/Key/Data/@Hash", doc, + XPathConstants.STRING); + byte[] verifiedHash = Hex.decodeHex(hashToCheck); + + boolean isHashVerified = Arrays.equals(Arrays.copyOf(computedHash, verifiedHash.length), + verifiedHash); + if (!isHashVerified) { + throw new IllegalStateException("Hash mismatch error"); + } + return hexData; + } + return Base64.decodeBase64(data.getBytes()); + } else { + //Any file compute the hash + MessageDigest md = Encryption.getSha256MessageDigestInstance(); + byte[] keyFile = md.digest(inputBytes); + return keyFile; } - return hexData; - } - return Base64.decodeBase64(data.getBytes()); + } catch (Exception e) { throw new RuntimeException("Key File input stream cannot be null"); } diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java index 4b8ea5f2..31b8801f 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java @@ -122,4 +122,40 @@ public void testSignatureFails() throws Exception { InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); toConsole(decryptedInputStream); } -} \ No newline at end of file + + /** + * Test KDBX with random KeyFile and key + */ + @Test + public void testKeyFileRandom() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("kdb_with_random_file.kdbx"); + InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("random_file"); + Credentials credentials = new KdbxCreds("123".getBytes(), inputStreamKeyFile); + InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); + toConsole(decryptedInputStream); + } + + /** + * Test KDBX with 64 bytes hex KeyFile and key + */ + @Test + public void testKeyFileHex64() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("kdbx_keyfile64.kdbx"); + InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("keyfile64"); + Credentials credentials = new KdbxCreds("123".getBytes(), inputStreamKeyFile); + InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); + toConsole(decryptedInputStream); + } + + /** + * Test KDBX with 32 bytes KeyFile and key + */ + @Test + public void testKeyFile32() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("kdbx_keyfile32.kdbx"); + InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("keyfile32"); + Credentials credentials = new KdbxCreds("123".getBytes(), inputStreamKeyFile); + InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); + toConsole(decryptedInputStream); + } +} diff --git a/test/src/main/resources/kdb_with_random_file.kdbx b/test/src/main/resources/kdb_with_random_file.kdbx new file mode 100755 index 0000000000000000000000000000000000000000..d46ac89f688252821f95da9170816cda1ed8e668 GIT binary patch literal 1870 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 IoE4#%+lJSJbpQYW literal 0 HcmV?d00001 diff --git a/test/src/main/resources/kdbx_keyfile64.kdbx b/test/src/main/resources/kdbx_keyfile64.kdbx new file mode 100755 index 0000000000000000000000000000000000000000..04adb6154f06813bd77237d177cb48d6123f2f71 GIT binary patch literal 1870 zcmV-U2eJ4A*`k_f`%AR}00RI55CAd3^5(yBLr}h01tDtuTK@wC0096100bZal6>zn zSuwo=BD1&tX3>_1HtL_(Q}k||{HEZCKkDVe1t0)gu@FWW=3Zm;-DUm$HF4UXsj?)yi1 z*jiwDv_w5L zQO@V>I0SnN1ONg60000401XNa3ec3HZ-uJ@E+K@a-7Eqti84tN-k`6<}n=TfN#1iaGVrvMJWw`w|4Qa+soRb#&qg_&i;1WY# zdO~27xPCL=|3|inHaH!!8RSp#QMOy+V)ut7CNBBN{VO+ip=lxGTr3-un4J&=`F zO@cwI8@O4S{|#fa56JtiASAA!*JISL^O3Qm29Xz02Yu--veAytrkY0YNbg}6Y7Yc> z)toSCnjvMTyvyIulw=2mqckc|oKugfk{PX)F6IC&LBQ^EiBF5SXym`YFQC2TzwvI) z5u(TfrST(=^X^oQV+r4Sujh4R@9ChUJ`g8R{J3@6L>1o4za4zeW?r)p_wu%0VW(Gqb z^~P@j0|;1PG%mgtfv}96sqw0F5~GjoxMF@U{er$EP-X-$;SZ+YhJu382aQL&Q>Ndb zhsl>I87!^)=58V1DMQ;5kK(QNIRmRiBN|H&o3xiKyQ4M>dt9tYPNUs4pw6e%hFnCV z*tLYQcdrRxa$+kL_%)dZ-n%k1n?#iwgN!+T$F$#@Y<6hbgmm(tKjU1k<=GUIdx6$R}CSq&@iplxu=nHo}RLtrr02AM?JU~s` zlBz#l4T0N&k@BJ{Qs{axl~Gw(8BG2vz{icu`DuGFFB;i2697|${q8vc56O1#^`8|= zjS)etWOY~7`4dRZLO~*EUP}M%XS9mS>n61mooLkB@Q>d@471oMO&QWaQjcd&h`mrE z5`^rLU}g%oIoeSoPp@V_FlE~kh7RTR)<`IjQ_KKg?5%SBfWr(?lbbmGUV)Cv4Gm=k zISHWWD5~e~<)>$$3F&$qck1DjH4p}nj z{tXWe7>)r`liE}2ueAsA;NZkbFWyE`A1}$HP~sc`@SSYbR=4 zv(kw!ml*57TJBGC=~znF@;jpA_SJ4;l0jwA#-XI*rYhuqYp($JJ;?#>LB9n5;|2~n zGo1m$q;$mSn45v*gP;?X$pU9sCeWcKolN92vDM6`sKyrFO;NzulwewOixy-f1$9;_HPoqbc35w{mHdLYn~iX183j~=FyIET?!!9 I8g}V263F3(TmS$7 literal 0 HcmV?d00001 diff --git a/test/src/main/resources/keyfile32 b/test/src/main/resources/keyfile32 new file mode 100755 index 00000000..70b28d84 --- /dev/null +++ b/test/src/main/resources/keyfile32 @@ -0,0 +1 @@ +KQQWFvUqScAobSfwq4z5MwnbITIb38vm \ No newline at end of file diff --git a/test/src/main/resources/keyfile64 b/test/src/main/resources/keyfile64 new file mode 100755 index 00000000..d41b5e88 --- /dev/null +++ b/test/src/main/resources/keyfile64 @@ -0,0 +1 @@ +bc63a755436af06b88027816446ba0486e8e15fac0e26dbaee82f662c77546bc \ No newline at end of file diff --git a/test/src/main/resources/random_file b/test/src/main/resources/random_file new file mode 100755 index 0000000000000000000000000000000000000000..a572bb478e4ff8b64b6726b70a0c86d461c58570 GIT binary patch literal 3072 zcmV+b4FB`qU)m;2eMp3MQhDHJdZtVgpGepEKcrJZE0%AJvvB-X21K%VxTof3t;X85 zEp)HIgQuY$z|TL`)g~@dLvq4G`@0c7@PwGynRVJA+*_nV%MC`3+3&*gS#vIYs73cu z%g2LZ1`tfTxEN*!Zp_?TR5N=+Jg4DRb$W_Hy)0SmB2 zbO}2m+m?6p-?SaJ$XV3o#bEoA3~R^(u)wfccwb#{A#;jes!Tt<5w$u@w>hN(C7jqA z5KWURI}@gg*BFuzpc9}ME?0b955js{n0%(r_uN`;lGdb?Rj~b23q2AO_%mvXuvSM@ z4!7GM>mly47fdroXvDrCHWpeNMG$1d0e9~C$$SPuYyC`w&;kv6_4aVW=_8&j zsR8-y9BNjFm5XCd2~o)Wt@EJfw$}eyp+K|FmsR$)7M0J}nX2nD%0vehB6{6nKE4!U zFH+$lAH|9&<<8!~5BR6FR&o}s_gvap>iCo``w;)zCDZwD8^H!agUV7ON4i;LJ?{iZbTze>1PbXFNd2G62S|eNK z#aCgPfGQ{~L%g%T*!&w^9NPexgXSeqewZUYk5n=#x0W4HRA_K!o@orV=xhk$SWV^U z)sNb+Ikd>rvW9iEo&r-nkSY03X?&+ znr#he_J$5_P5Nec@*Wr_bl6`f9_mUF4-Tcq(Svez_M`qIpN^`e;CNiKC4Ey*#XZc# zatm=|u+;fB4ig>#GPeP8jpdX7M1ZG!kE0ibKMJ1hRq8W2}Qm_x0no!No z4AK|eT`)?AWP)eVYEN`tMN=4TRkhOS2KFM25q(M>l6Cl;d`j$xvsm!7*<2c3K_kE` zDvW#6goA9=SM<%pB+|HH2y70&i)G66f3QP^!<6-CsT@3JrqsV^Giu<`>0Rc^W!cNQ zdQl-zfzV&D%VJ}LN8qykq)!Dx7rCRM}uWxQst!7)p2)}IVh0Q!`0ozN7XHu){X=tD$$R^qR+A44`H)Nx0~Bm|GyE; zU)n-Z7HjpwOZB#JDn|OS8kNA4(*P0XH5m^0l6zpkXu$x*1bR|=5Uj+kF&X|@glw@r zi}cn=12n_Oc&eB)+f700#!MmMDr`EpBdMQuePtAP3v7O}`uaS6^ir9ByVh)h+q0`F zWI+a}P$dhPl!VjtTp=tlouB1P2MYpxHu#Ip`CGO>DmN3bVzuFu#umpKVFG4OYe`m| zpalBdp%W=R4(;zCJaBd7qkL#-DvYwNZ$;HX4Qfqiamdcx8PkfDEKI%PQXI5tM;f?`$Yu813} zXqYj4_2v7|BxT*RxwTz1?-Puk+Wq|u&dW{R>tvnmrx}+~NORxH4CI(D$463W3F$%u z7mKLReB1qJV%o-Zo8i|reM$gw{}t=Y4no^V7@n6A%~Pp>e#THw;=OcfyOAI{-rWd) zCIy7g@6HV(q5Ac7jFHx4m1+G&!|i2F!K;*|_e1}wv+EiAA|+t||3k>LoMn=!dPxS* zfsgyyN?5`dYDe)|MI67KxP|Jh1}IIpyK*U zeHl|T)R5bOP(S2xK{q|SioY3#v!(#Aq@wrg1&X;*DI<78XelCKpuR|kToWHH>_}NU zsmlK|uz9$Gx|KA%jv*3ltMNo2w7@e-*MsVfzOAWx9C+=MJ2&@9=I1!y@`-h|nB9&yZ0hHQym?oa zs`t{&RVTNW2?yN`f9Vzx*P#G97zl2vd^v(UVZ(liyZ(6?T7q!Ul%kUEA;M|H2Q0@e zvcp+bib-Sg3;tbY@`mcw?Y#)>qgPTXG?LA8(?jc6nQxtZAZ7!kbRyVM{4Ty`ZOF21 zjEgJfr?^%cjU&6mU9u9C4_ZUE&wAp?;Y1OV?-y&urnjDOdk7#zy^+m;+2ILld-k1G zIgHq{zdUg^@QASC9PhEUd>Xmzl_1Hl%|d-JA2wLAv|bN7neo<-;}8vXEI;GGuGA90 zT4r7dwuAR?z|p*Idsvl@w-i;cVw|=OFzClZB#y}!^2%0^ZVYjr0MLG{ieU06=0<1j z{3rwcBL>ilZb)`*6nEWA(rkgL>||h`XT0np9}`!&FI9)LW88)(lisQ$^TU zbfsw>JtnN7Y)aUVNHt+9FI3OP@ukt%`RenXSmPLWyPytc@2gejC_W^28XVb{lhfuP zq$%M9o1goW$bW`_8&mV#{dKJ#B-_G(xHwJif#k(Q$Bv+;{w`Pyc+TE0&SQZ|>Z6Cv zn_c0iH?cjC+o(VH|B*AKT3OdZ>wU&UfDYgQ=wB`YNqP{8UVJI$W>z#ECQj?$k+#3z z(P7njlRs|xYX6X&u2HY;S%k)lj^ir|sRZ|fj3;VIO;oR#**&{^!VGg*a;|ZOIPzI3 z@BB$Z4;iI5DF&x}_Ek Date: Sat, 23 Sep 2023 18:14:06 +0200 Subject: [PATCH 164/228] #41 code review for PR --- .../org/linguafranca/pwdb/kdbx/Helpers.java | 44 +----- .../linguafranca/pwdb/kdbx/KdbxKeyFile.java | 148 ++++++++++++------ 2 files changed, 99 insertions(+), 93 deletions(-) diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java index e66a063a..d789bd15 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java @@ -23,23 +23,17 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; + import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.text.SimpleDateFormat; import java.time.*; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.Date; -import java.util.TimeZone; import java.util.UUID; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; /** * The class provides helpers to marshal and unmarshal values of KDBX files @@ -207,40 +201,4 @@ public static byte[] toBytes(int value, ByteOrder byteOrder) { .putInt(value); return longBuffer; } - - /** - * Check if the data is an XML file. - * - * @param data the file to check - * @return true if and only if the data is an XML parsable - */ - public static boolean checkIfKeyFileIsXml(byte[] data) { - try { - InputStream is = new ByteArrayInputStream(data); - DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - documentBuilder.parse(is); - return true; - } catch (Exception e) { - return false; - } - } - - /** - * Check if the data is a valid hex - * - * @param data the data to check - * @return true if and only if the key contained in the KeyFile is an Hex format - * @throws IllegalAccessException if the length of the data is not equal to 64 - */ - public static boolean isValidHexString(byte[] data) { - - if(data == null) { - return false; - } - - final Pattern hexPattern = Pattern.compile("\\p{XDigit}+"); - final Matcher matcher = hexPattern.matcher(new String(data)); - return matcher.matches(); - } - } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java index ea5c7269..0c3c1157 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java @@ -16,26 +16,33 @@ package org.linguafranca.pwdb.kdbx; +import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Hex; import org.linguafranca.pwdb.security.Encryption; import org.w3c.dom.Document; +import org.xml.sax.SAXException; + -import com.google.common.io.ByteStreams; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamReader; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; +import java.io.PushbackInputStream; import java.security.MessageDigest; import java.util.Arrays; - - /** * Class has a static method to load a key from a KDBX XML Key File * @@ -45,70 +52,111 @@ public class KdbxKeyFile { private static XPath xpath = XPathFactory.newInstance().newXPath(); + private static int BUFFER_SIZE = 65; + private static int KEY_LEN_32 = 32; + private static int KEY_LEN_64 = 64; /** * Load a key from an InputStream with a KDBX XML key file. + * * @param inputStream the input stream holding the key * @return the key */ public static byte[] load(InputStream inputStream) { - try { - byte[] inputBytes = ByteStreams.toByteArray(inputStream); - if (inputBytes.length == 32) { - //32 bytes KeyFile - return inputBytes; - } else if (inputBytes.length == 64) { - //64 bytes KeyFile (only Hexadecimal values) - if (Helpers.isValidHexString(inputBytes)) { - byte[] keyFile = org.bouncycastle.util.encoders.Hex.decode(inputBytes); - return keyFile; - } else { - throw new IllegalStateException("KeyFile contains not allowed characters"); - } - } else { - //Standard KeyFile - if (Helpers.checkIfKeyFileIsXml(inputBytes)) { - - InputStream is = new ByteArrayInputStream(inputBytes); - DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - Document doc = documentBuilder.parse(is); - String version = (String) xpath.evaluate("//KeyFile/Meta/Version/text()", doc, - XPathConstants.STRING); - String data = (String) xpath.evaluate("//KeyFile/Key/Data/text()", doc, XPathConstants.STRING); - if (data == null) { - return null; - } - if (version.equals("2.0")) { + PushbackInputStream pis = new PushbackInputStream(inputStream, BUFFER_SIZE); + byte[] buffer = new byte[65]; + int bytesRead = pis.read(buffer); - byte[] hexData = Hex.decodeHex(data.replaceAll("\\s", "")); - - MessageDigest md = Encryption.getSha256MessageDigestInstance(); - byte[] computedHash = md.digest(hexData); - - String hashToCheck = (String) xpath.evaluate("//KeyFile/Key/Data/@Hash", doc, - XPathConstants.STRING); - byte[] verifiedHash = Hex.decodeHex(hashToCheck); - - boolean isHashVerified = Arrays.equals(Arrays.copyOf(computedHash, verifiedHash.length), - verifiedHash); - if (!isHashVerified) { - throw new IllegalStateException("Hash mismatch error"); - } - return hexData; - } - return Base64.decodeBase64(data.getBytes()); + if (bytesRead == KEY_LEN_32) { + return Arrays.copyOf(buffer, bytesRead); + } else if (bytesRead == KEY_LEN_64) { + byte[] keyFile = Hex.decodeHex(new String(Arrays.copyOf(buffer, bytesRead))); + return keyFile; + } else { + if (isXML(buffer)) { + pis.unread(buffer); // Push back the buffer + return computeXmlKeyFile(pis); } else { - //Any file compute the hash + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(buffer, 0, bytesRead); // Insert the first 65 bytes in the OutputStream + buffer = new byte[1024]; // Increase the buffer + while ((bytesRead = pis.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + // Compute the SHA256 of the InputStream MessageDigest md = Encryption.getSha256MessageDigestInstance(); - byte[] keyFile = md.digest(inputBytes); + byte[] keyFile = md.digest(outputStream.toByteArray()); return keyFile; } } + } catch (IOException | DecoderException e) { + throw new IllegalArgumentException(e); + } + } + /** + * Check if the data is an XML + * @param data the data to ckeck + * @return true if the data is an XML + */ + private static boolean isXML(byte[] data) { + + try { + XMLInputFactory factory = XMLInputFactory.newFactory(); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + XMLStreamReader reader = factory.createXMLStreamReader(bais); + // Attempt to read the start element of the XML document. + if (reader.hasNext()) { + int eventType = reader.next(); + if (eventType == XMLStreamReader.START_ELEMENT) { + return true; // The InputStream contains valid XML + } + } + // If we reach this point, it's not valid XML. + return false; } catch (Exception e) { - throw new RuntimeException("Key File input stream cannot be null"); + // An exception occurred, so it's not valid XML. + return false; + } + } + + /** + * Read the InputStream (keyx file) and compute the hash (SHA2-256) to build a key + * + * @param is The KeyFile as an InputStream + * @return the computed byte array (keyFile) to compute the MasterKey + */ + private static byte[] computeXmlKeyFile(InputStream is) { + + try { + DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document doc = documentBuilder.parse(new PushbackInputStream(is)); // This function close the input stream, we + // need to create another one + String version = (String) xpath.evaluate("//KeyFile/Meta/Version/text()", doc, XPathConstants.STRING); + String data = (String) xpath.evaluate("//KeyFile/Key/Data/text()", doc, XPathConstants.STRING); + if (data == null) { + return null; + } + if (version.equals("2.0")) { + byte[] hexData = Hex.decodeHex(data.replaceAll("\\s", "")); + MessageDigest md = Encryption.getSha256MessageDigestInstance(); + byte[] computedHash = md.digest(hexData); + String hashToCheck = (String) xpath.evaluate("//KeyFile/Key/Data/@Hash", doc, XPathConstants.STRING); + byte[] verifiedHash = Hex.decodeHex(hashToCheck); + + boolean isHashVerified = Arrays.equals(Arrays.copyOf(computedHash, verifiedHash.length), + verifiedHash); + if (!isHashVerified) { + throw new IllegalStateException("Hash mismatch error"); + } + return hexData; + } + return Base64.decodeBase64(data.getBytes()); + + } catch(IOException | SAXException | ParserConfigurationException | XPathExpressionException | DecoderException e) { + throw new IllegalArgumentException("An error occours during XML parsing: " + e.getMessage()); } } } From e295bfaef53d5f99a190219e3cb984bc89922f02 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 25 Sep 2023 18:31:00 +0100 Subject: [PATCH 165/228] Suggested KeyFile processing --- .../linguafranca/pwdb/kdbx/KdbxKeyFile.java | 98 +++++++++++++++++-- 1 file changed, 89 insertions(+), 9 deletions(-) diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java index 9d2b52e5..23a03df0 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java @@ -16,16 +16,28 @@ package org.linguafranca.pwdb.kdbx; +import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Hex; +import org.linguafranca.pwdb.security.Encryption; import org.w3c.dom.Document; +import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; +import java.io.FilterInputStream; +import java.io.IOException; import java.io.InputStream; +import java.io.PushbackInputStream; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Objects; /** * Class has a static method to load a key from a KDBX XML Key File @@ -35,28 +47,96 @@ @SuppressWarnings("WeakerAccess") public class KdbxKeyFile { - private static XPath xpath = XPathFactory.newInstance().newXPath(); + private static final XPath xpath = XPathFactory.newInstance().newXPath(); + private static final int BUFFER_SIZE = 65; + private static final int KEY_LEN_32 = 32; + private static final int KEY_LEN_64 = 64; + + private static class HashMismatchException extends Exception {} /** - * Load a key from an InputStream with a KDBX XML key file. - * @param inputStream the input stream holding the key + * Load a key from an InputStream + *

    + * The InputStream can represent ... TODO write about the formats + * + * @param inputStream the input stream holding the key, caller should close * @return the key */ public static byte[] load(InputStream inputStream) { + DigestInputStream digestInputStream = new DigestInputStream(inputStream, Encryption.getSha256MessageDigestInstance()); + PushbackInputStream pis = new PushbackInputStream(digestInputStream, BUFFER_SIZE); + try { + byte[] buffer = new byte[65]; + int bytesRead = pis.read(buffer); + if (bytesRead == KEY_LEN_32) { + // if length 32 assume binary key file + return buffer; + } else if (bytesRead == KEY_LEN_64) { + // if length 64 assume hex encoded key file + return Hex.decodeHex(new String(Arrays.copyOf(buffer, bytesRead))); + } else { + // if length not 32 or 64 either an XML key file or just a file to get digest + pis.unread(buffer); + try { + // see if it's an XML key file + pis.unread(buffer); + return tryComputeXmlKeyFile(new FilterInputStream(pis){ + @Override + public void close() { + // suppress ability to close, so we can carry on reading on exception + } + }); + } catch (HashMismatchException e) { + throw new IllegalArgumentException("Invalid key in signature file"); + } catch (Exception e) { + byte [] sink = new byte[1024]; + // xml file was invalid so read the remainder of file + //noinspection StatementWithEmptyBody + while (digestInputStream.read(sink) > 0) { + // just reading file to get its digest + } + return digestInputStream.getMessageDigest().digest(); + } + } + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + + + /** + * Read the InputStream (kdbx xml keyfile) and compute the hash (SHA-256) to build a key + * + * @param is The KeyFile as an InputStream + * @return the computed byte array (keyFile) to compute the MasterKey + */ + private static byte[] tryComputeXmlKeyFile(InputStream is) throws HashMismatchException { + try { DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - Document doc = documentBuilder.parse(inputStream); + Document doc = documentBuilder.parse(is); String version = (String) xpath.evaluate("//KeyFile/Meta/Version/text()", doc, XPathConstants.STRING); String data = (String) xpath.evaluate("//KeyFile/Key/Data/text()", doc, XPathConstants.STRING); if (data == null) { return null; } - if (version.equals("2.0")) { - return Hex.decodeHex(data.replaceAll("\\s","")); + if (Objects.isNull(version) || !version.equals("2.0")){ + return Base64.decodeBase64(data.getBytes()); } - return Base64.decodeBase64(data.getBytes()); - } catch (Exception e) { - throw new RuntimeException("Key File input stream cannot be null"); + + byte[] hexData = Hex.decodeHex(data.replaceAll("\\s", "")); + MessageDigest md = Encryption.getSha256MessageDigestInstance(); + byte[] computedHash = md.digest(hexData); + + String hashToCheck = (String) xpath.evaluate("//KeyFile/Key/Data/@Hash", doc, XPathConstants.STRING); + byte[] verifiedHash = Hex.decodeHex(hashToCheck); + + if (!Arrays.equals(Arrays.copyOf(computedHash, verifiedHash.length), verifiedHash)) { + throw new HashMismatchException(); + } + return hexData; + } catch(IOException | SAXException | ParserConfigurationException | XPathExpressionException | DecoderException e) { + throw new IllegalArgumentException("An error occurred during XML parsing: " + e.getMessage()); } } } From 291365a4028c0475398126339918bb78ec9775aa Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 25 Sep 2023 18:33:19 +0100 Subject: [PATCH 166/228] Suggested KeyFile processing (edit class header) --- kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java index 23a03df0..b16fab74 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java @@ -40,7 +40,7 @@ import java.util.Objects; /** - * Class has a static method to load a key from a KDBX XML Key File + * Class has a static method to load a key from an {@link InputStream} * * @author jo */ From 264d3dda6ee8dd1f1f0a35e615f7688720996657 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 25 Sep 2023 20:24:39 +0100 Subject: [PATCH 167/228] correct base64 decode, avoid string creation for 64 bit file --- .../main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java index b16fab74..8d41fc50 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java @@ -34,6 +34,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; +import java.nio.ByteBuffer; import java.security.DigestInputStream; import java.security.MessageDigest; import java.util.Arrays; @@ -72,11 +73,10 @@ public static byte[] load(InputStream inputStream) { // if length 32 assume binary key file return buffer; } else if (bytesRead == KEY_LEN_64) { - // if length 64 assume hex encoded key file - return Hex.decodeHex(new String(Arrays.copyOf(buffer, bytesRead))); + // if length 64 assume hex encoded key file (avoid creating a String) + return Hex.decodeHex(ByteBuffer.wrap(buffer).asCharBuffer().array()); } else { // if length not 32 or 64 either an XML key file or just a file to get digest - pis.unread(buffer); try { // see if it's an XML key file pis.unread(buffer); @@ -121,7 +121,7 @@ private static byte[] tryComputeXmlKeyFile(InputStream is) throws HashMismatchEx return null; } if (Objects.isNull(version) || !version.equals("2.0")){ - return Base64.decodeBase64(data.getBytes()); + return Base64.decodeBase64(data); } byte[] hexData = Hex.decodeHex(data.replaceAll("\\s", "")); From c80df9eb20440c358f16023f21ff422d4100d49a Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 25 Sep 2023 22:11:32 +0100 Subject: [PATCH 168/228] tidying --- .../linguafranca/pwdb/kdbx/KdbxKeyFile.java | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java index 8d41fc50..ec920fac 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java @@ -42,8 +42,6 @@ /** * Class has a static method to load a key from an {@link InputStream} - * - * @author jo */ @SuppressWarnings("WeakerAccess") public class KdbxKeyFile { @@ -53,21 +51,20 @@ public class KdbxKeyFile { private static final int KEY_LEN_32 = 32; private static final int KEY_LEN_64 = 64; - private static class HashMismatchException extends Exception {} - /** * Load a key from an InputStream *

    - * The InputStream can represent ... TODO write about the formats + * The InputStream can represent ... TODO write about the formats * * @param inputStream the input stream holding the key, caller should close * @return the key */ public static byte[] load(InputStream inputStream) { - DigestInputStream digestInputStream = new DigestInputStream(inputStream, Encryption.getSha256MessageDigestInstance()); + DigestInputStream digestInputStream = new DigestInputStream(inputStream, + Encryption.getSha256MessageDigestInstance()); PushbackInputStream pis = new PushbackInputStream(digestInputStream, BUFFER_SIZE); try { - byte[] buffer = new byte[65]; + byte[] buffer = new byte[BUFFER_SIZE]; int bytesRead = pis.read(buffer); if (bytesRead == KEY_LEN_32) { // if length 32 assume binary key file @@ -80,30 +77,29 @@ public static byte[] load(InputStream inputStream) { try { // see if it's an XML key file pis.unread(buffer); - return tryComputeXmlKeyFile(new FilterInputStream(pis){ + return tryComputeXmlKeyFile(new FilterInputStream(pis) { + // suppress ability to close, so we can carry on reading on exception @Override - public void close() { - // suppress ability to close, so we can carry on reading on exception - } + public void close() { /* nothing */ } }); } catch (HashMismatchException e) { throw new IllegalArgumentException("Invalid key in signature file"); - } catch (Exception e) { - byte [] sink = new byte[1024]; - // xml file was invalid so read the remainder of file - //noinspection StatementWithEmptyBody - while (digestInputStream.read(sink) > 0) { - // just reading file to get its digest - } - return digestInputStream.getMessageDigest().digest(); + } catch (Exception ignored) { + // fall through } + // xml file was invalid so read the remainder of file + byte[] sink = new byte[1024]; + // read file to get its digest + //noinspection StatementWithEmptyBody + while (digestInputStream.read(sink) > 0) { /* nothing */ } + return digestInputStream.getMessageDigest().digest(); + } } catch (Exception e) { throw new IllegalArgumentException(e); } } - /** * Read the InputStream (kdbx xml keyfile) and compute the hash (SHA-256) to build a key * @@ -120,7 +116,7 @@ private static byte[] tryComputeXmlKeyFile(InputStream is) throws HashMismatchEx if (data == null) { return null; } - if (Objects.isNull(version) || !version.equals("2.0")){ + if (Objects.isNull(version) || !version.equals("2.0")) { return Base64.decodeBase64(data); } @@ -135,8 +131,12 @@ private static byte[] tryComputeXmlKeyFile(InputStream is) throws HashMismatchEx throw new HashMismatchException(); } return hexData; - } catch(IOException | SAXException | ParserConfigurationException | XPathExpressionException | DecoderException e) { + } catch (IOException | SAXException | ParserConfigurationException | XPathExpressionException | + DecoderException e) { throw new IllegalArgumentException("An error occurred during XML parsing: " + e.getMessage()); } } + + private static class HashMismatchException extends Exception { + } } From 2d930f472c0cc16e76673195b5730aa2c8f53240 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 26 Sep 2023 09:40:13 +0100 Subject: [PATCH 169/228] more improvements --- .../linguafranca/pwdb/kdbx/KdbxKeyFile.java | 85 +++++++++++-------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java index ec920fac..5f543306 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java @@ -60,42 +60,48 @@ public class KdbxKeyFile { * @return the key */ public static byte[] load(InputStream inputStream) { + // wrap the stream to get its digest (in case we need it) DigestInputStream digestInputStream = new DigestInputStream(inputStream, Encryption.getSha256MessageDigestInstance()); + // wrap the stream, so we can test reading from it but then push back to get original stream PushbackInputStream pis = new PushbackInputStream(digestInputStream, BUFFER_SIZE); try { byte[] buffer = new byte[BUFFER_SIZE]; int bytesRead = pis.read(buffer); + + // if length 32 assume binary key file if (bytesRead == KEY_LEN_32) { - // if length 32 assume binary key file return buffer; - } else if (bytesRead == KEY_LEN_64) { - // if length 64 assume hex encoded key file (avoid creating a String) - return Hex.decodeHex(ByteBuffer.wrap(buffer).asCharBuffer().array()); - } else { - // if length not 32 or 64 either an XML key file or just a file to get digest + } + + // if length 64 may be hex encoded key file + if (bytesRead == KEY_LEN_64) { try { - // see if it's an XML key file - pis.unread(buffer); - return tryComputeXmlKeyFile(new FilterInputStream(pis) { - // suppress ability to close, so we can carry on reading on exception - @Override - public void close() { /* nothing */ } - }); - } catch (HashMismatchException e) { - throw new IllegalArgumentException("Invalid key in signature file"); - } catch (Exception ignored) { - // fall through + return Hex.decodeHex(ByteBuffer.wrap(buffer).asCharBuffer().array()); // (avoid creating a String) + } catch (DecoderException ignored) { + // fall through it may be an XML file or just a file whose digest we want } - // xml file was invalid so read the remainder of file - byte[] sink = new byte[1024]; - // read file to get its digest - //noinspection StatementWithEmptyBody - while (digestInputStream.read(sink) > 0) { /* nothing */ } - return digestInputStream.getMessageDigest().digest(); + } + // restore stream + pis.unread(buffer); + // if length not 32 or 64 either an XML key file or just a file to get digest + try { + // see if it's an XML key file + return tryComputeXmlKeyFile(pis); + } catch (HashMismatchException e) { + throw new IllegalArgumentException("Invalid key in signature file"); + } catch (Exception ignored) { + // fall through to get file digest } - } catch (Exception e) { + + // is not a valid xml file, so read the remainder of file + byte[] sink = new byte[1024]; + // read file to get its digest + //noinspection StatementWithEmptyBody + while (digestInputStream.read(sink) > 0) { /* nothing */ } + return digestInputStream.getMessageDigest().digest(); + } catch (IOException e) { throw new IllegalArgumentException(e); } } @@ -103,34 +109,43 @@ public void close() { /* nothing */ } /** * Read the InputStream (kdbx xml keyfile) and compute the hash (SHA-256) to build a key * - * @param is The KeyFile as an InputStream + * @param is The KeyFile as an InputStream, must return with stream open on error * @return the computed byte array (keyFile) to compute the MasterKey */ private static byte[] tryComputeXmlKeyFile(InputStream is) throws HashMismatchException { - + // DocumentBuilder closes input stream so wrap inputStream to inhibit this in case of failure + InputStream unCloseable = new FilterInputStream(is) { + @Override + public void close() { /* nothing */ } + }; try { DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - Document doc = documentBuilder.parse(is); - String version = (String) xpath.evaluate("//KeyFile/Meta/Version/text()", doc, XPathConstants.STRING); + Document doc = documentBuilder.parse(unCloseable); + // get the key String data = (String) xpath.evaluate("//KeyFile/Key/Data/text()", doc, XPathConstants.STRING); if (data == null) { - return null; + throw new IllegalArgumentException("Key file does not contain a key"); } + // get the file version + String version = (String) xpath.evaluate("//KeyFile/Meta/Version/text()", doc, XPathConstants.STRING); + // if not 2.0 then key is base64 encoded if (Objects.isNull(version) || !version.equals("2.0")) { return Base64.decodeBase64(data); } - byte[] hexData = Hex.decodeHex(data.replaceAll("\\s", "")); - MessageDigest md = Encryption.getSha256MessageDigestInstance(); - byte[] computedHash = md.digest(hexData); + // key data may contain white space + byte[] decodedData = Hex.decodeHex(data.replaceAll("\\s", "")); + byte[] decodedDataHash = Encryption.getSha256MessageDigestInstance().digest(decodedData); + // hash used to verify the data String hashToCheck = (String) xpath.evaluate("//KeyFile/Key/Data/@Hash", doc, XPathConstants.STRING); - byte[] verifiedHash = Hex.decodeHex(hashToCheck); + byte[] decodedHashToCheck = Hex.decodeHex(hashToCheck); - if (!Arrays.equals(Arrays.copyOf(computedHash, verifiedHash.length), verifiedHash)) { + // hashToCheck is a truncated version of the actual hash + if (!Arrays.equals(Arrays.copyOf(decodedDataHash, decodedHashToCheck.length), decodedHashToCheck)) { throw new HashMismatchException(); } - return hexData; + return decodedData; } catch (IOException | SAXException | ParserConfigurationException | XPathExpressionException | DecoderException e) { throw new IllegalArgumentException("An error occurred during XML parsing: " + e.getMessage()); From bb7c5e2febd5938f44309e63ffd95aa046b79b64 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 26 Sep 2023 09:55:58 +0100 Subject: [PATCH 170/228] nits --- kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java | 1 - 1 file changed, 1 deletion(-) diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java index 5f543306..010ecd7a 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java @@ -36,7 +36,6 @@ import java.io.PushbackInputStream; import java.nio.ByteBuffer; import java.security.DigestInputStream; -import java.security.MessageDigest; import java.util.Arrays; import java.util.Objects; From 765d4a27f0a89fc8af3cbda123bb1f0b49e7921b Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 6 Sep 2024 14:41:02 +0100 Subject: [PATCH 171/228] Update documentation in preparation for release of 2.2.2 --- CHANGELOG.md | 5 +++-- DatabaseStructure.svg | 4 ++-- readme.md | 35 ++++++++++++++++++++--------------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a791c2db..99723723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,12 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.com) and [Semantic Versioning](http://semver.org/spec/v2.0.0.html) -## [2.2.2-SNAPSHOT] +## [2.2.2] 2024-09-06 ### Added -- implementation of database using Jackson +- implementation of database using Jackson via @giusvale-dev +- enhancement of KeyFile support via @giusvale-dev ## [2.2.1] 2023-08-21 diff --git a/DatabaseStructure.svg b/DatabaseStructure.svg index 20a5ade1..0694e7aa 100644 --- a/DatabaseStructure.svg +++ b/DatabaseStructure.svg @@ -1,4 +1,4 @@ - + -
    Save/Load
    Save/Load
    SerializableDatabase
    (DOM, JAXB, Simple)
    SerializableDatabase...
    Encrypt/Decrypt
    Encrypt/Decrypt
    StreamFormat
    (Kdbx, None)
    StreamFormat...
    Read/Write Values
    Read/Write Values
    Save/Load
    Save/Load
    Database Implementation
    (Memory Model, DOM etc.)
    Database Implementation...
    Save/Load
    Save/Load
    User Read/Write
    User Read/Write
    Database Facade
    (DOM Wrapper, JAXB, SImple)
    Database Facade...
    Text is not SVG - cannot display
    \ No newline at end of file +
    Save/Load
    Save/Load
    SerializableDatabase
    (Jackson, DOM, JAXB, Simple)
    SerializableDatabase...
    Encrypt/Decrypt
    Encrypt/Decrypt
    StreamFormat
    (Kdbx, None)
    StreamFormat...
    Read/Write Values
    Read/Write Values
    Save/Load
    Save/Load
    Database Implementation
    (Memory Model, DOM etc.)
    Database Implementation...
    Save/Load
    Save/Load
    User Read/Write
    User Read/Write
    Database Facade
    (Jackson, DOM Wrapper, JAXB, SImple)
    Database Facade...
    Text is not SVG - cannot display
    \ No newline at end of file diff --git a/readme.md b/readme.md index d7a208b0..0a46a161 100644 --- a/readme.md +++ b/readme.md @@ -41,12 +41,14 @@ It is licensed under the Apache 2 License and is currently usable. After a period of neglect, the project is (May 2023) back in development. -The current code is version 2.2.2-SNAPSHOT. This is on the main branch. See [Build from Source](#build-from-source) +The current code is version 2.2.2. This is on the main branch. See [Build from Source](#build-from-source) Key updates relative to 2.1: - Java 8 (dependencies no longer support Java 7) -- Updated dependencies to remove known vulnerabilities - File format version 4 support - with Argon2 +- Inclusion of Jackson based KDBX support with a view to removing SimpleXML and JAXB support +- Updated keyfile support +- Updated dependencies to remove known vulnerabilities See the [changelog](CHANGELOG.md) for more details. @@ -54,11 +56,11 @@ See the [changelog](CHANGELOG.md) for more details. ### Release -The composite POM for the last release (2.2.1), Java 8 compatible, is +The composite POM for the last release (2.2.2), Java 8 compatible, is org.linguafranca.pwdb KeePassJava2 - 2.2.1 + 2.2.2 at Maven Central. Note that the artifactId has become Camel Case from release 2.1.x onwards. @@ -68,7 +70,7 @@ Snapshot builds are erratically available at [Sonatype](https://oss.sonatype.org org.linguafranca.pwdb KeePassJava2 - 2.2.2-SNAPSHOT + 2.2.3-SNAPSHOT with appropriate `` entry, like: @@ -212,7 +214,7 @@ For Java 11 and later Jaxb implementation depends on explicit inclusion [no long - [JAXB](https://javaee.github.io/jaxb-v2/) -It also depends on SLF4J and Junit for tests. +It also depends on SLF4J and Junit 4 for tests. ## Build from Source @@ -256,9 +258,17 @@ Each module corresponds to a Maven artifact. The GroupId is `org.linguafranca.pw kdbxKeePassJava2-kdbx Javadocs Provides support for KDBX streaming and security. + + +jacksonKeePassJava2-jackson +Javadocs +A Jackson based implementation of KDBX. Intended to provide the main support going forward, replacing trhe +modules below. + simpleKeePassJava2-simple Javadocs -A Simple XML Platform implementation of KDBX. Could be useful for Android. +A Simple XML Platform implementation of KDBX. Could be useful for Android. +Simple cannot be used with Java versions 17 and up. jaxbKeePassJava2-jaxb Javadocs @@ -268,12 +278,7 @@ bindings might be useful for building other interfaces. domKeePassJava2-dom Javadocs A DOM based implementation of KDBX. Being DOM based it is rather slow, but -messes less with existing content than the other two implementations. Known to work on Android. - -domKeePassJava2-jackson -Javadocs -A Jackson based implementation of KDBX. Intended to replace the Simple XML implementation. Simple XML seems no -longer to be maintained. +messes less with existing content than the other implementations. Known to work on Android. @@ -301,13 +306,13 @@ Many thanks to Pavel Ivanov [@ivanovpv](https://github.com/ivanovpv) for his help with Android and Gradle compatibility issues. Thanks to Giuseppe Valente [@giusvale-dev](https://github.com/giusvale-dev) for -the contribution of the Jackson module. +the contribution of the Jackson module and enhancements to KeyFile support. Thanks to other contributors and raisers of issues. ## License -Copyright (c) 2023 Jo Rabin +Copyright (c) 2024 Jo Rabin Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From f5da4c401fc5e4d3f0a6623ce1fefea6987039f6 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 6 Sep 2024 15:54:45 +0100 Subject: [PATCH 172/228] Updated dependencies --- CHANGELOG.md | 4 ++++ database/pom.xml | 2 ++ jackson/pom.xml | 4 ++-- jaxb/pom.xml | 2 +- kdb/pom.xml | 3 ++- kdbx/pom.xml | 6 ++++-- pom.xml | 5 +++-- simple/pom.xml | 2 +- 8 files changed, 19 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99723723..92ddc6c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.co - implementation of database using Jackson via @giusvale-dev - enhancement of KeyFile support via @giusvale-dev +## Changed + +- Updated dependencies (leave jaxb and guava as is) + ## [2.2.1] 2023-08-21 ### Added diff --git a/database/pom.xml b/database/pom.xml index 67b6015d..4cc80fa2 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -16,10 +16,12 @@ org.jetbrains annotations + 24.1.0 com.google.guava guava + 33.2.1-android org.bouncycastle diff --git a/jackson/pom.xml b/jackson/pom.xml index 0f1d8021..f4d9fbc5 100644 --- a/jackson/pom.xml +++ b/jackson/pom.xml @@ -36,12 +36,12 @@ com.fasterxml.jackson.dataformat jackson-dataformat-xml - 2.15.0 + 2.17.1 com.fasterxml.woodstox woodstox-core - 6.5.0 + 7.0.0 org.linguafranca.pwdb diff --git a/jaxb/pom.xml b/jaxb/pom.xml index fbe8e56a..271fea28 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -43,7 +43,7 @@ com.sun.xml.bind jaxb-impl - 2.3.3 + 2.3.9 org.linguafranca.pwdb diff --git a/kdb/pom.xml b/kdb/pom.xml index 611a3231..b6a0e660 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -21,7 +21,8 @@ org.jetbrains annotations - + 24.1.0 + org.linguafranca.pwdb test diff --git a/kdbx/pom.xml b/kdbx/pom.xml index c642ea8d..5709bed0 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -41,11 +41,13 @@ commons-codec commons-codec - + 1.17.0 + org.jetbrains annotations - + 24.1.0 + org.linguafranca.pwdb test diff --git a/pom.xml b/pom.xml index b6f03e3a..e7a898ce 100644 --- a/pom.xml +++ b/pom.xml @@ -159,6 +159,7 @@ org.slf4j slf4j-api + 2.0.12 ch.qos.logback @@ -182,7 +183,7 @@ org.bouncycastle bcpkix-jdk18on - 1.76 + 1.78.1 org.slf4j @@ -194,7 +195,7 @@ ch.qos.logback logback-classic - 1.3.7 + 1.3.14 commons-codec diff --git a/simple/pom.xml b/simple/pom.xml index 4fbe068d..323f4024 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -53,7 +53,7 @@ org.apache.httpcomponents httpcore - 4.4.15 + 4.4.16 com.fasterxml From f847aa2ff2bb0d6129831ebdb575c8adcc3983d0 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 6 Sep 2024 17:49:12 +0100 Subject: [PATCH 173/228] Added IgnoreMe class to otherwise empty "all" module to workaround Javadoc complaint --- all/src/main/java/org/linguafranca/pwdb/IgnoreMe.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 all/src/main/java/org/linguafranca/pwdb/IgnoreMe.java diff --git a/all/src/main/java/org/linguafranca/pwdb/IgnoreMe.java b/all/src/main/java/org/linguafranca/pwdb/IgnoreMe.java new file mode 100644 index 00000000..0f2521c5 --- /dev/null +++ b/all/src/main/java/org/linguafranca/pwdb/IgnoreMe.java @@ -0,0 +1,7 @@ +package org.linguafranca.pwdb; + +/** + * This is here for Javadoc generation purposes only + */ +public class IgnoreMe { +} From 7daa86ce45be7dc000575a8572e0143451e362a1 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 6 Sep 2024 17:49:52 +0100 Subject: [PATCH 174/228] Update to 2.2.2 and release --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jackson/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 2 +- simple/pom.xml | 2 +- test/pom.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index 892f7056..7f8cad39 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2-SNAPSHOT + 2.2.2 ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index 4cc80fa2..99ed1e28 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2-SNAPSHOT + 2.2.2 ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index f3ccfe79..8adb92ad 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2-SNAPSHOT + 2.2.2 ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index bc807577..1e3cf1ca 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2-SNAPSHOT + 2.2.2 ../pom.xml 4.0.0 diff --git a/jackson/pom.xml b/jackson/pom.xml index f4d9fbc5..b6ea4010 100644 --- a/jackson/pom.xml +++ b/jackson/pom.xml @@ -18,7 +18,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2-SNAPSHOT + 2.2.2 ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 271fea28..a24e3d4e 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2-SNAPSHOT + 2.2.2 ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index b6a0e660..2d32517d 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2-SNAPSHOT + 2.2.2 ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index 5709bed0..a68e31b6 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2-SNAPSHOT + 2.2.2 ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index e7a898ce..f6a98a35 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.2.2-SNAPSHOT + 2.2.2 database test diff --git a/simple/pom.xml b/simple/pom.xml index 323f4024..3d39c667 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2-SNAPSHOT + 2.2.2 ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index 47961c4c..bbf2d020 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2-SNAPSHOT + 2.2.2 4.0.0 From 5e28095533f8238967b0461c6974dabaf89fb18c Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 6 Sep 2024 19:06:32 +0100 Subject: [PATCH 175/228] correct badge to 2.2.2 --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 0a46a161..df2709f5 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent) [![javadoc](https://javadoc.io/badge2/org.linguafranca.pwdb/KeePassJava2/javadoc.svg)](https://javadoc.io/doc/org.linguafranca.pwdb/KeePassJava2) -![alt text](https://badgen.net/badge/Build/2.2.2-SNAPSHOT/blue?icon=github) +![alt text](https://badgen.net/badge/Build/2.2.2/blue?icon=github) master [![CircleCI](https://dl.circleci.com/status-badge/img/gh/jorabin/KeePassJava2/tree/master.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/jorabin/KeePassJava2/tree/master) develop [![CircleCI](https://dl.circleci.com/status-badge/img/gh/jorabin/KeePassJava2/tree/develop.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/jorabin/KeePassJava2/tree/develop) From cec0c2c35234da8dd51c6acc3613405a1bd8fe38 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 7 Sep 2024 11:16:05 +0100 Subject: [PATCH 176/228] Update to next development iteration 2.2.3-SNAPSHOT --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jackson/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 2 +- simple/pom.xml | 2 +- test/pom.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index 7f8cad39..a0748435 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2 + 2.2.3-SNAPSHOT ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index 99ed1e28..f4095dec 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2 + 2.2.3-SNAPSHOT ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index 8adb92ad..674166f4 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2 + 2.2.3-SNAPSHOT ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index 1e3cf1ca..cbf75d8b 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2 + 2.2.3-SNAPSHOT ../pom.xml 4.0.0 diff --git a/jackson/pom.xml b/jackson/pom.xml index b6ea4010..9726b910 100644 --- a/jackson/pom.xml +++ b/jackson/pom.xml @@ -18,7 +18,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2 + 2.2.3-SNAPSHOT ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index a24e3d4e..535b53de 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2 + 2.2.3-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index 2d32517d..f78d55f9 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2 + 2.2.3-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index a68e31b6..282eeed4 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2 + 2.2.3-SNAPSHOT ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index f6a98a35..e74cd2b5 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.2.2 + 2.2.3-SNAPSHOT database test diff --git a/simple/pom.xml b/simple/pom.xml index 3d39c667..6e8e5078 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2 + 2.2.3-SNAPSHOT ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index bbf2d020..529ff049 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.2 + 2.2.3-SNAPSHOT 4.0.0 From eece4ca05737733e54c9cfa661ec9dc839a7402b Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 7 Sep 2024 12:08:34 +0100 Subject: [PATCH 177/228] Add explanatory note for fixed IV --- .../java/org/linguafranca/pwdb/security/StreamEncryptor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/database/src/main/java/org/linguafranca/pwdb/security/StreamEncryptor.java b/database/src/main/java/org/linguafranca/pwdb/security/StreamEncryptor.java index 6fc32ed2..b3fc4a15 100644 --- a/database/src/main/java/org/linguafranca/pwdb/security/StreamEncryptor.java +++ b/database/src/main/java/org/linguafranca/pwdb/security/StreamEncryptor.java @@ -73,6 +73,7 @@ public ChaCha20(byte[] key) { } class Salsa20 extends Default { + // This fixed iv shows up as a vulnerability but is part of the definition of Keepass private static final byte[] SALSA20_IV = Hex.decode("E830094B97205D2A".getBytes()); public Salsa20(byte[] key) { From c0f6cae3a7dd63804b622557686e2444de79947a Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 7 Sep 2024 12:09:18 +0100 Subject: [PATCH 178/228] Correcting Jackson tests --- .../org/linguafranca/pwdb/JacksonBinaryPropertyV3Test.java | 2 +- .../org/linguafranca/pwdb/JacksonBinaryPropertyV4Test.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV3Test.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV3Test.java index 4760d509..41c14567 100644 --- a/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV3Test.java +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV3Test.java @@ -52,7 +52,7 @@ public Database newDatabase() { try { result = new JacksonDatabase(); } catch(Exception e) { - e.printStackTrace(); + throw new RuntimeException(e); } return result; diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV4Test.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV4Test.java index 1ae7ecba..ec65bdc4 100644 --- a/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV4Test.java +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV4Test.java @@ -51,8 +51,8 @@ public Database newDatabase() { Database result = null; try { result = new JacksonDatabase(); - } catch(Exception e) { - e.printStackTrace(); + } catch (Exception e) { + throw new RuntimeException(e); } return result; From 6516d8616baaf3a74677f5d49eef30b720c06b70 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 7 Sep 2024 17:43:59 +0100 Subject: [PATCH 179/228] Format/typo --- .../linguafranca/pwdb/kdbx/KdbxKeyFile.java | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java index 9b8ba10c..c3f7004e 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java @@ -54,18 +54,35 @@ public class KdbxKeyFile { private static final int KEY_LEN_64 = 64; /** - * Load a key from an InputStream, in this method the inputStrem represent the KeyFile + * Load a key from an InputStream, in this method the inputStream represents the KeyFile *

    - * A key file is a file that contains a key (and possibly additional data, e.g. a hash that allows to verify the integrity of the key). The file extension typically is 'keyx' or 'key'. + * A key file is a file that contains a key (and possibly additional data, e.g. a hash that allows to verify the + * integrity of the key). The file extension typically is 'keyx' or 'key'. *

    + *

    + * The following comes from KeePass Help Files: + *

    + * * Formats. KeePass supports the following key file formats: *
      - *
    • XML (recommended, default). There is an XML format for key files. KeePass 2.x uses this format by default, i.e. when creating a key file in the master key dialog, an XML key file is created. The syntax and the semantics of the XML format allow to detect certain corruptions (especially such caused by faulty hardware or transfer problems), and a hash (in XML key files version 2.0 or higher) allows to verify the integrity of the key. This format is resistant to most encoding and new-line character changes (which is useful for instance when the user is opening and saving the key file or when transferring it from/to a server). Such a key file can be printed (as a backup on paper), and comments can be added in the file (with the usual XML syntax: ). It is the most flexible format; new features can be added easily in the future.
    • - *
    • 32 bytes. If the key file contains exactly 32 bytes, these are used as a 256-bit cryptographic key. This format requires the least disk space.
    • - *
    • Hexadecimal. If the key file contains exactly 64 hexadecimal characters (0-9 and A-F, in UTF-8/ASCII encoding, one line, no spaces), these are decoded to a 256-bit cryptographic key.
    • - *
    • Hashed. If a key file does not match any of the formats above, its content is hashed using a cryptographic hash function in order to build a key (typically a 256-bit key with SHA-256). This allows to use arbitrary files as key files.
    • + *
    • XML (recommended, default). There is an XML format for key files. KeePass 2.x uses this format by + * default, i.e. when creating a key file in the master key dialog, an XML key file is created. The syntax + * and the semantics of the XML format allow to detect certain corruptions (especially such caused by faulty + * hardware or transfer problems), and a hash (in XML key files version 2.0 or higher) allows to verify the + * integrity of the key. This format is resistant to most encoding and new-line character changes (which is + * useful for instance when the user is opening and saving the key file or when transferring it from/to a + * server). Such a key file can be printed (as a backup on paper), and comments can be added in the file + * (with the usual XML syntax: ). It is the most flexible format; new features can be added + * easily in the future.
    • + *
    • 32 bytes. If the key file contains exactly 32 bytes, these are used as a 256-bit cryptographic key. + * This format requires the least disk space.
    • + *
    • Hexadecimal. If the key file contains exactly 64 hexadecimal characters (0-9 and A-F, + * in UTF-8/ASCII encoding, one line, no spaces), these are decoded to a 256-bit cryptographic key.
    • + *
    • Hashed. If a key file does not match any of the formats above, its content is hashed using a + * cryptographic hash function in order to build a key (typically a 256-bit key with SHA-256). + * This allows to use arbitrary files as key files.
    • *
    - * + *
    * @param inputStream the input stream holding the key, caller should close * @return the key */ From 19e873f9f0ee43c05cc1ded86880ed9d4d4bf79c Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 9 Sep 2024 15:36:03 +0100 Subject: [PATCH 180/228] Refactor Abstract Database and Entry to throw UnsupportedOperationException for PropertyValue storage. Refactor PropertyValue with default implementations Move MavenRunListener to Database to support unit tests in that module --- database/pom.xml | 6 +- .../java/org/linguafranca/pwdb/Database.java | 22 ++- .../org/linguafranca/pwdb/PropertyValue.java | 175 ++++++++++++------ .../pwdb/base/AbstractDatabase.java | 58 ++++-- .../linguafranca/pwdb/base/AbstractEntry.java | 23 ++- .../pwdb/protect/ProtectedDatabase.java | 53 ++++++ .../linguafranca/util/MavenRunListener.java | 0 .../linguafranca/pwdb/PropertyValueTest.java | 49 +++++ example/pom.xml | 5 + .../SecureCharBufferPropertyValue.java | 76 ++++++++ .../linguafranca/pwdb/kdbx/QuickStart.java | 7 +- readme.md | 3 +- 12 files changed, 381 insertions(+), 96 deletions(-) create mode 100644 database/src/main/java/org/linguafranca/pwdb/protect/ProtectedDatabase.java rename {test => database}/src/main/java/org/linguafranca/util/MavenRunListener.java (100%) create mode 100644 database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java create mode 100644 example/src/main/java/org/linguafranca/pwdb/example/SecureCharBufferPropertyValue.java diff --git a/database/pom.xml b/database/pom.xml index f9e33b39..c4cf081f 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -28,9 +28,9 @@ bcpkix-jdk18on - io.github.novacrypto - SecureString - 2022.01.17 + org.apache.commons + commons-lang3 + 3.15.0 \ No newline at end of file diff --git a/database/src/main/java/org/linguafranca/pwdb/Database.java b/database/src/main/java/org/linguafranca/pwdb/Database.java index 58394576..632f35ae 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Database.java +++ b/database/src/main/java/org/linguafranca/pwdb/Database.java @@ -177,14 +177,14 @@ public interface Database , G extends Group visitor); /** * Visit all entries starting from a group * @param group the group to start at * @param visitor the visitor to use */ - void visit(G group, Visitor visitor); + void visit(G group, Visitor visitor); /** * Find all entries that match the criteria @@ -264,15 +264,18 @@ public interface Database , G extends Group getShouldProtect(); + List listShouldProtect(); + + /** + * Get the default means of storage of unprotected and protected property values + */ + public PropertyValue.Strategy getPropertyValueStrategy(); /** * Set the default means of storage of unprotected and protected property values - * @param unprotectedPropertyFactory a builder for unprotected properties - * @param protectedPropertyFactory a builder for protected properties + * @param propertyValueStrategy a propertyValue strategy */ - void setPropertyValueStrategy(PropertyValue.Factory unprotectedPropertyFactory, - PropertyValue.Factory protectedPropertyFactory); + void setPropertyValueStrategy(PropertyValue.Strategy propertyValueStrategy); /** * returns true if the database supports non-standard property names @@ -289,4 +292,9 @@ void setPropertyValueStrategy(PropertyValue.Factory unprotectedPropertyFactory, */ boolean supportsRecycleBin(); + /** + * returns true if the implementation supports a PropertyValueStrategy + */ + boolean supportsPropertyValueStrategy(); + } diff --git a/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java b/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java index bceabeea..cc0f3292 100644 --- a/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java +++ b/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java @@ -1,10 +1,14 @@ package org.linguafranca.pwdb; -import io.github.novacrypto.SecureCharBuffer; +import org.apache.commons.lang3.CharSequenceUtils; +import javax.crypto.*; + +import java.io.Serializable; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; +import java.security.Key; /** * An interface through which property values can be stored in memory to make it @@ -17,6 +21,8 @@ public interface PropertyValue { byte [] getValueAsBytes(); + CharSequence getValue(); + boolean isProtected(); /** @@ -30,64 +36,59 @@ interface Factory { PropertyValue of (byte [] value); } - CharSequence getValue(); - - /** - * Unprotected value does not use String - */ - - class Default implements PropertyValue { - - private final CharBuffer value; + interface Strategy { + PropertyValue.Factory getUnprotectectedValueFactory (); + PropertyValue.Factory getProtectectedValueFactory (); + } + class StringStore implements PropertyValue { + private final String value; static class Factory implements PropertyValue.Factory { @Override - public PropertyValue of(CharSequence aCharSequence) { - return new Default(aCharSequence); + public StringStore of(CharSequence aCharSequence) { + return new StringStore(aCharSequence); } @Override - public PropertyValue of(char[] value) { - return new Default(value); + public StringStore of(char[] value) { + return new StringStore(value); } @Override - public PropertyValue of(byte[] value) { - return new Default(value); + public StringStore of(byte[] value) { + return new StringStore(value); } } - public Default(CharSequence aString) { - this.value = CharBuffer.wrap(aString); + public StringStore(CharSequence aCharSequence){ + this.value = String.valueOf(aCharSequence); } - - public Default(char [] value) { - this.value = CharBuffer.wrap(value); + public StringStore(char[] value){ + this.value = String.valueOf(value); } - - public Default(byte [] value) { - this.value = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(value)); + public StringStore(byte[] value){ + this.value = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(value)).toString(); } @Override public String getValueAsString() { - return this.value.toString(); + return value; } @Override - public CharSequence getValue() { - return this.value; + public char[] getValueAsChars() { + return value.toCharArray(); } @Override - public char [] getValueAsChars() { - return this.value.array(); + public byte[] getValueAsBytes() { + return value.getBytes(StandardCharsets.UTF_8); } @Override - public byte [] getValueAsBytes() { - return StandardCharsets.UTF_8.encode(value).array(); + public CharSequence getValue() { + return value; } @Override @@ -95,67 +96,125 @@ public boolean isProtected() { return false; } } - - /** - * Protected Value uses {@link SecureCharBuffer} + * Unprotected value does not use String */ - class Protected implements PropertyValue { - private final SecureCharBuffer value; + class CharsStore implements PropertyValue, Serializable { + + private final char[] value; - /** - * Builder for {@link PropertyValue.Protected} - */ static class Factory implements PropertyValue.Factory { @Override - public PropertyValue of(CharSequence aString) { - return new Protected(aString); + public CharsStore of(CharSequence aCharSequence) { + return new CharsStore(aCharSequence); } @Override - public PropertyValue of(char[] value) { - return new Protected(value); + public CharsStore of(char[] value) { + return new CharsStore(value); } @Override - public PropertyValue of(byte[] value) { - return new Protected(value); + public CharsStore of(byte[] value) { + return new CharsStore(value); } } - public Protected(CharSequence aString) { - this.value = SecureCharBuffer.withCapacity(aString.length()); - this.value.append(aString); + public CharsStore(CharSequence aString) { + this.value = CharSequenceUtils.toCharArray(aString); } - public Protected(char [] value) { - this.value = SecureCharBuffer.withCapacity(value.length); - this.value.append(CharBuffer.wrap(value)); + public CharsStore(char [] value) { + this.value = value; } - public Protected(byte [] value) { - this.value = SecureCharBuffer.withCapacity(value.length); - this.value.append(StandardCharsets.UTF_8.decode(ByteBuffer.wrap(value))); + public CharsStore(byte [] value) { + this.value = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(value)).array(); } + @Override public String getValueAsString() { - return this.value.toStringAble().toString(); + return CharBuffer.wrap(this.value).toString(); } @Override public CharSequence getValue() { - return this.value; + return CharBuffer.wrap(this.value); } @Override public char [] getValueAsChars() { - return CharBuffer.wrap(value).array(); + return this.value; } @Override public byte [] getValueAsBytes() { - return StandardCharsets.UTF_8.encode(CharBuffer.wrap(value)).array(); + return StandardCharsets.UTF_8.encode(CharBuffer.wrap(this.value)).array(); + } + + @Override + public boolean isProtected() { + return false; + } + } + + /** + * Encrypted property value storage intended for storing passwords in something other than + * plaintext using {@link javax.crypto.SealedObject} class. + *

    + * Since the key and the sealed object are stored together this is a bit of a vulnerability. + */ + class SealedStore implements PropertyValue { + static KeyGenerator keyGenerator; + private final SealedObject sealedObject; + + ByteBuffer buffer; + private final Key key; + + static { + try { + keyGenerator= KeyGenerator.getInstance("AES"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public SealedStore(CharsStore object){ + try { + key = keyGenerator.generateKey(); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, key); + sealedObject = new SealedObject(object, cipher); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + private CharsStore getAsDefault() { + try { + return ((CharsStore) sealedObject.getObject(key)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + @Override + public String getValueAsString() { + return getAsDefault().getValueAsString(); + } + + @Override + public char[] getValueAsChars() { + return getAsDefault().getValueAsChars(); + } + + @Override + public byte[] getValueAsBytes() { + return getAsDefault().getValueAsBytes(); + } + + @Override + public CharSequence getValue() { + return getAsDefault().getValue(); } @Override diff --git a/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java b/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java index 595220f0..b0ef0e60 100644 --- a/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java +++ b/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java @@ -19,6 +19,7 @@ import org.linguafranca.pwdb.*; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.UUID; @@ -41,14 +42,14 @@ public void setDirty(boolean dirty) { } @Override - public void visit(Visitor visitor) { + public void visit(Visitor visitor) { visitor.startVisit(getRootGroup()); visit(getRootGroup(), visitor); visitor.endVisit(getRootGroup()); } @Override - public void visit(G group, Visitor visitor) { + public void visit(G group, Visitor visitor) { if (visitor.isEntriesFirst()) { for (E entry : group.getEntries()) { @@ -63,7 +64,7 @@ public void visit(G group, Visitor visitor) { } if (!visitor.isEntriesFirst()) { - for (Entry entry : group.getEntries()) { + for (E entry : group.getEntries()) { visitor.visit(entry); } } @@ -87,7 +88,7 @@ public G newGroup(String name) { } @Override - public G newGroup(Group group) { + public G newGroup(Group group) { G result = newGroup(); result.setName(group.getName()); result.setIcon(this.newIcon(group.getIcon().getIndex())); @@ -131,16 +132,11 @@ public E newEntry(Entry entry) { @Override public E findEntry(final UUID uuid) { - List entries = findEntries(new Entry.Matcher() { - @Override - public boolean matches(Entry entry) { - return entry.getUuid().equals(uuid); - } - }); + List entries = findEntries(entry -> entry.getUuid().equals(uuid)); if (entries.size() > 1) { throw new IllegalStateException("Two entries same UUID"); } - if (entries.size() == 0) { + if (entries.isEmpty()) { return null; } return entries.get(0); @@ -165,14 +161,13 @@ public boolean deleteEntry(final UUID uuid) { @Override public G findGroup(final UUID uuid){ final List groups = new ArrayList<>(); - visit(new Visitor.Default() { + visit(new Visitor.Default() { // set to true while visiting sub groups of recycle bin boolean recycle; @Override - public void startVisit(Group group) { + public void startVisit(G group) { if (!recycle && group.getUuid().equals(uuid)) { - //noinspection unchecked - groups.add((G) group); + groups.add(group); } if (group.isRecycleBin()) { recycle = true; @@ -180,7 +175,7 @@ public void startVisit(Group group) { } @Override - public void endVisit(Group group) { + public void endVisit(G group) { if (group.isRecycleBin()) { recycle = false; } @@ -189,7 +184,7 @@ public void endVisit(Group group) { if (groups.size() > 1) { throw new IllegalStateException("Two groups same UUID"); } - if (groups.size() == 0) { + if (groups.isEmpty()) { return null; } return groups.get(0); @@ -239,4 +234,33 @@ public boolean supportsBinaryProperties() { public boolean supportsRecycleBin() { return true; } + + @Override + public boolean shouldProtect(String propertyName){ + throw new UnsupportedOperationException(); + } + + @Override + public void setShouldProtect(String propertyName, boolean protect){ + throw new UnsupportedOperationException(); + } + + @Override + public List listShouldProtect(){ + throw new UnsupportedOperationException(); + } + + @Override + public PropertyValue.Strategy getPropertyValueStrategy(){ + throw new UnsupportedOperationException(); + } + @Override + public void setPropertyValueStrategy(PropertyValue.Strategy strategy){ + throw new UnsupportedOperationException(); + } + @Override + public boolean supportsPropertyValueStrategy(){ + return false; + } + } diff --git a/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java b/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java index 3cdc4e35..8c159d8d 100644 --- a/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java +++ b/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java @@ -16,10 +16,7 @@ package org.linguafranca.pwdb.base; -import org.linguafranca.pwdb.Database; -import org.linguafranca.pwdb.Entry; -import org.linguafranca.pwdb.Group; -import org.linguafranca.pwdb.Icon; +import org.linguafranca.pwdb.*; /** * Base implementation of Entry @@ -127,5 +124,23 @@ public void setNotes(String notes) { touch(); } + @Override + public char[] getPropertyAsChars(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public PropertyValue getPropertyValue(String name) { + throw new UnsupportedOperationException(); + } + @Override + public void setProperty(String name, PropertyValue value) { + throw new UnsupportedOperationException(); + } + + @Override + public void setProperty(String name, char[] value) { + throw new UnsupportedOperationException(); + } protected abstract void touch(); } diff --git a/database/src/main/java/org/linguafranca/pwdb/protect/ProtectedDatabase.java b/database/src/main/java/org/linguafranca/pwdb/protect/ProtectedDatabase.java new file mode 100644 index 00000000..9d210196 --- /dev/null +++ b/database/src/main/java/org/linguafranca/pwdb/protect/ProtectedDatabase.java @@ -0,0 +1,53 @@ +package org.linguafranca.pwdb.protect; + +import org.linguafranca.pwdb.*; +import org.linguafranca.pwdb.base.AbstractDatabase; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public abstract class ProtectedDatabase, G extends Group, E extends Entry, I extends Icon> extends AbstractDatabase { + private final List protectedProperties = new ArrayList<>(); + private PropertyValue.Strategy valueStrategy; + + @Override + public boolean shouldProtect(String propertyName){ + return protectedProperties.contains(propertyName); + } + + @Override + public void setShouldProtect(String propertyName, boolean protect){ + if (protect) { + protectedProperties.add(propertyName); + } else { + protectedProperties.remove(propertyName); + } + } + + @Override + public List listShouldProtect(){ + return Collections.unmodifiableList(protectedProperties); + } + + /** + * Get the default means of storage of unprotected and protected property values + */ + @Override + public PropertyValue.Strategy getPropertyValueStrategy(){ + return this.valueStrategy; + } + /** + * Set the default means of storage of unprotected and protected property values + * @param strategy a property value strategy + */ + @Override + public void setPropertyValueStrategy(PropertyValue.Strategy strategy){ + this.valueStrategy = strategy; + } + + @Override + public boolean supportsPropertyValueStrategy(){ + return true; + } +} diff --git a/test/src/main/java/org/linguafranca/util/MavenRunListener.java b/database/src/main/java/org/linguafranca/util/MavenRunListener.java similarity index 100% rename from test/src/main/java/org/linguafranca/util/MavenRunListener.java rename to database/src/main/java/org/linguafranca/util/MavenRunListener.java diff --git a/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java b/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java new file mode 100644 index 00000000..253072ae --- /dev/null +++ b/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java @@ -0,0 +1,49 @@ +package org.linguafranca.pwdb; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; + +import static org.junit.Assert.*; + +public class PropertyValueTest { + + public static final String THIS_IS_A_SECRET = "This is a secret + לַחַיִּים"; + Logger logger = LoggerFactory.getLogger(PropertyValueTest.class); + @Test + public void sealedObjectTest() { + PropertyValue.CharsStore.Factory factory = new PropertyValue.CharsStore.Factory(); + + PropertyValue.CharsStore testValue = factory.of(THIS_IS_A_SECRET); + PropertyValue.SealedStore sealed = new PropertyValue.SealedStore(testValue); + assertEquals(testValue.getValue(), sealed.getValue()); + assertArrayEquals(testValue.getValueAsBytes(), sealed.getValueAsBytes()); + assertArrayEquals(testValue.getValueAsChars(), sealed.getValueAsChars()); + assertEquals(testValue.getValueAsString(), sealed.getValueAsString()); + + testValue = factory.of(THIS_IS_A_SECRET.getBytes()); + sealed = new PropertyValue.SealedStore(testValue); + assertArrayEquals(testValue.getValueAsBytes(), sealed.getValueAsBytes()); + assertArrayEquals(testValue.getValueAsChars(), sealed.getValueAsChars()); + assertEquals(testValue.getValueAsString(), sealed.getValueAsString()); + + testValue = factory.of(THIS_IS_A_SECRET.toCharArray()); + sealed = new PropertyValue.SealedStore(testValue); + assertArrayEquals(testValue.getValueAsBytes(), sealed.getValueAsBytes()); + assertArrayEquals(testValue.getValueAsChars(), sealed.getValueAsChars()); + assertEquals(testValue.getValueAsString(), sealed.getValueAsString()); + } + @Test + public void NoOpTest() { + PropertyValue.StringStore.Factory factory = new PropertyValue.StringStore.Factory(); + + PropertyValue.StringStore testValue = factory.of(THIS_IS_A_SECRET); + assertEquals(THIS_IS_A_SECRET, testValue.getValueAsString()); + testValue = factory.of(THIS_IS_A_SECRET.toCharArray()); + assertEquals(THIS_IS_A_SECRET, testValue.getValueAsString()); + testValue = factory.of(THIS_IS_A_SECRET.getBytes(StandardCharsets.UTF_8)); + assertEquals(THIS_IS_A_SECRET, testValue.getValueAsString()); + } +} \ No newline at end of file diff --git a/example/pom.xml b/example/pom.xml index cbf75d8b..7cfde215 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -43,6 +43,11 @@ test ${project.version} + + io.github.novacrypto + SecureString + 2022.01.17 + diff --git a/example/src/main/java/org/linguafranca/pwdb/example/SecureCharBufferPropertyValue.java b/example/src/main/java/org/linguafranca/pwdb/example/SecureCharBufferPropertyValue.java new file mode 100644 index 00000000..0b84d4ae --- /dev/null +++ b/example/src/main/java/org/linguafranca/pwdb/example/SecureCharBufferPropertyValue.java @@ -0,0 +1,76 @@ +package org.linguafranca.pwdb.example; + +import io.github.novacrypto.SecureCharBuffer; +import org.linguafranca.pwdb.PropertyValue; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; + +/** + * SecureCharBufferPropertyValue uses {@link SecureCharBuffer} + */ +public class SecureCharBufferPropertyValue implements PropertyValue { + private final SecureCharBuffer value; + + /** + * Builder for {@link SecureCharBufferPropertyValue} + */ + static class Factory implements PropertyValue.Factory { + + @Override + public PropertyValue of(CharSequence aString) { + return new SecureCharBufferPropertyValue(aString); + } + + @Override + public PropertyValue of(char[] value) { + return new SecureCharBufferPropertyValue(value); + } + + @Override + public PropertyValue of(byte[] value) { + return new SecureCharBufferPropertyValue(value); + } + } + + public SecureCharBufferPropertyValue(CharSequence aString) { + this.value = SecureCharBuffer.withCapacity(aString.length()); + this.value.append(aString); + } + + public SecureCharBufferPropertyValue(char [] value) { + this.value = SecureCharBuffer.withCapacity(value.length); + this.value.append(CharBuffer.wrap(value)); + } + + public SecureCharBufferPropertyValue(byte [] value) { + this.value = SecureCharBuffer.withCapacity(value.length); + this.value.append(StandardCharsets.UTF_8.decode(ByteBuffer.wrap(value))); + } + @Override + public String getValueAsString() { + return this.value.toStringAble().toString(); + } + + @Override + public CharSequence getValue() { + return this.value; + } + + @Override + public char [] getValueAsChars() { + return CharBuffer.wrap(value).array(); + } + + @Override + public byte [] getValueAsBytes() { + return StandardCharsets.UTF_8.encode(CharBuffer.wrap(value)).array(); + } + + @Override + public boolean isProtected() { + return true; + } +} + diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java index e6c7206c..c68a93b3 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java @@ -104,12 +104,7 @@ public void splice(G newParent, Group groupToSplice) { * Group by title */ public G groupByTitle(D database) { - List entries = database.findEntries(new Entry.Matcher() { - @Override - public boolean matches(Entry entry) { - return entry.getProperty(Entry.STANDARD_PROPERTY_NAME_TITLE).toLowerCase().contains("findme!"); - } - }); + List entries = database.findEntries(entry -> entry.getProperty(Entry.STANDARD_PROPERTY_NAME_TITLE).toLowerCase().contains("findme!")); // create a new group using DB factory method G newParent = database.newGroup("Found entries"); // iterate over the found entries diff --git a/readme.md b/readme.md index df2709f5..96aedd71 100644 --- a/readme.md +++ b/readme.md @@ -180,7 +180,8 @@ There is also a discussion of the [enhancements in KDBX 4.1](https://keepass.inf as a discussion of [Key Files](https://keepass.info/help/base/keys.html#keyfiles). Massive credit also to the folks over at [KeePassXC](https://keepassxc.org/) who wrote some -[documentation](https://github.com/keepassxreboot/keepassxc-specs) about their understanding of various format things. +[documentation](https://github.com/keepassxreboot/keepassxc-specs) about their understanding of various format things. Also, this is a +useful [discussion/investigation](https://github.com/scubajorgen/KeepassDecrypt) of the KDBX format. For the sake of clarification and my own satisfaction I have written about my understanding of From 82144c3d15f17e66be4c55d0a261c84f48689506 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 9 Sep 2024 17:56:44 +0100 Subject: [PATCH 181/228] Tidy and simplify --- .../java/org/linguafranca/pwdb/Entry.java | 29 +------ .../org/linguafranca/pwdb/PropertyValue.java | 80 ++++++++++++++++--- .../linguafranca/pwdb/base/AbstractEntry.java | 10 +-- .../pwdb/protect/ProtectedDatabase.java | 23 +++++- .../pwdb/kdbx/jackson/model/EntryClasses.java | 17 ++-- 5 files changed, 97 insertions(+), 62 deletions(-) diff --git a/database/src/main/java/org/linguafranca/pwdb/Entry.java b/database/src/main/java/org/linguafranca/pwdb/Entry.java index dc75db5a..7c7d5818 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Entry.java +++ b/database/src/main/java/org/linguafranca/pwdb/Entry.java @@ -96,7 +96,7 @@ interface Matcher { /** * Gets the value of a property as a String. Use of this method is not recommended for fields with protected values. - * Use {@link #getPropertyAsChars(String)} or {@link #getPropertyValue(String)} + * Use {@link #getPropertyValue(String)} * *

    All implementations of Entry are required to support reading and writing of * {@link #STANDARD_PROPERTY_NAMES}. @@ -106,17 +106,6 @@ interface Matcher { */ String getProperty(String name); - /** - * Gets the value of a property as a char array. - * - *

    All implementations of Entry are required to support reading and writing of - * {@link #STANDARD_PROPERTY_NAMES}. - * @param name the name of the property to get - * @return a value or null if the property is not known, or if setting of arbitrary properties is not supported - * @see Database#supportsNonStandardPropertyNames() - */ - char [] getPropertyAsChars(String name); - /** * Gets the value of a property as a PropertyValue. * @@ -130,7 +119,7 @@ interface Matcher { /** * Sets the value of a property. Use of this method is not recommended for fields with protected values. - * Use {@link #setProperty(String, char[])} or {@link #setProperty(String, PropertyValue)} + * Use {@link #setProperty(String, PropertyValue)} * *

    Other than the {@link #STANDARD_PROPERTY_NAMES} support for this method is optional. * @@ -156,20 +145,6 @@ interface Matcher { */ void setProperty(String name, PropertyValue value); - /** - * Sets the value of a property using a char array. The method checks whether the value should be - * stored as a protected value or not by reference to {@link Database#shouldProtect(String)} - * - *

    Other than the {@link #STANDARD_PROPERTY_NAMES} support for this method is optional. - * - * @param name the name of the property to set - * @param value the value to set it to - * @throws UnsupportedOperationException if the name is not one of the standard properties and - * non-standard properties are not supported - * @see Database#supportsNonStandardPropertyNames() - */ - void setProperty(String name, char [] value); - /** * Removes this non-standard property, if it exists. * diff --git a/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java b/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java index cc0f3292..a5ebf87d 100644 --- a/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java +++ b/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java @@ -4,15 +4,16 @@ import javax.crypto.*; -import java.io.Serializable; +import java.io.*; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.security.Key; /** - * An interface through which property values can be stored in memory to make it - * harder to access their values via a heap dump etc. + * An interface through which (textual) property values can be stored in memory as something other than String + * and using various techniques for obfuscating the value and to make it + * harder to access the values via a heap dump etc. */ public interface PropertyValue { String getValueAsString(); @@ -26,7 +27,7 @@ public interface PropertyValue { boolean isProtected(); /** - * A builder interface for PropertyValue + * A factory interface for PropertyValue. */ interface Factory { PropertyValue of (CharSequence aCharSequence); @@ -36,14 +37,20 @@ interface Factory { PropertyValue of (byte [] value); } + /** + * A specification of which factories are to be used for unprotected values as opposed to protected values. + */ interface Strategy { PropertyValue.Factory getUnprotectectedValueFactory (); PropertyValue.Factory getProtectectedValueFactory (); } + /** + * Values are stored as strings. + */ class StringStore implements PropertyValue { private final String value; - static class Factory implements PropertyValue.Factory { + public static class Factory implements PropertyValue.Factory { @Override public StringStore of(CharSequence aCharSequence) { @@ -96,14 +103,15 @@ public boolean isProtected() { return false; } } + /** - * Unprotected value does not use String + * Property values are stored as char arrays. */ class CharsStore implements PropertyValue, Serializable { private final char[] value; - static class Factory implements PropertyValue.Factory { + public static class Factory implements PropertyValue.Factory { @Override public CharsStore of(CharSequence aCharSequence) { @@ -163,14 +171,15 @@ public boolean isProtected() { * Encrypted property value storage intended for storing passwords in something other than * plaintext using {@link javax.crypto.SealedObject} class. *

    - * Since the key and the sealed object are stored together this is a bit of a vulnerability. + * The Key for the encrypted value is stored off-heap. + *

    + * The overhead of using this class for encryption of the value then serialization of the key + * for storage off-heap may be significant. */ class SealedStore implements PropertyValue { static KeyGenerator keyGenerator; private final SealedObject sealedObject; - - ByteBuffer buffer; - private final Key key; + private final ByteBuffer buffer; static { try { @@ -180,9 +189,28 @@ class SealedStore implements PropertyValue { } } + public static class Factory implements PropertyValue.Factory { + + @Override + public SealedStore of(CharSequence aCharSequence) { + return new SealedStore(new CharsStore(aCharSequence)); + } + + @Override + public SealedStore of(char[] value) { + return new SealedStore(new CharsStore(value)); + } + + @Override + public SealedStore of(byte[] value) { + return new SealedStore(new CharsStore(value)); + } + } + public SealedStore(CharsStore object){ try { - key = keyGenerator.generateKey(); + Key key = keyGenerator.generateKey(); + buffer = storeKey(key); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, key); sealedObject = new SealedObject(object, cipher); @@ -190,9 +218,35 @@ public SealedStore(CharsStore object){ throw new RuntimeException(e); } } + + private ByteBuffer storeKey(Key key) throws IOException { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(key); + ByteBuffer b = ByteBuffer.allocateDirect(baos.size()); + b.put(baos.toByteArray()); + return b; + } + } + + private Key retrieveKey(ByteBuffer buffer) { + byte [] bytes = new byte[buffer.position()]; + buffer.rewind(); + buffer.get(bytes); + try { + try ( + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais)) { + return (Key) ois.readObject(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + private CharsStore getAsDefault() { try { - return ((CharsStore) sealedObject.getObject(key)); + return ((CharsStore) sealedObject.getObject(retrieveKey(buffer))); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java b/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java index 8c159d8d..e71186f2 100644 --- a/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java +++ b/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java @@ -124,23 +124,15 @@ public void setNotes(String notes) { touch(); } - @Override - public char[] getPropertyAsChars(String name) { - throw new UnsupportedOperationException(); - } - @Override public PropertyValue getPropertyValue(String name) { throw new UnsupportedOperationException(); } + @Override public void setProperty(String name, PropertyValue value) { throw new UnsupportedOperationException(); } - @Override - public void setProperty(String name, char[] value) { - throw new UnsupportedOperationException(); - } protected abstract void touch(); } diff --git a/database/src/main/java/org/linguafranca/pwdb/protect/ProtectedDatabase.java b/database/src/main/java/org/linguafranca/pwdb/protect/ProtectedDatabase.java index 9d210196..1d9773e1 100644 --- a/database/src/main/java/org/linguafranca/pwdb/protect/ProtectedDatabase.java +++ b/database/src/main/java/org/linguafranca/pwdb/protect/ProtectedDatabase.java @@ -4,12 +4,31 @@ import org.linguafranca.pwdb.base.AbstractDatabase; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; +/** + * Base class for Databases which support storage using {@link PropertyValue}s. + *

    + * By default {@link Entry#STANDARD_PROPERTY_NAME_PASSWORD} is defined as protected and the property value strategy + * establishes {@link org.linguafranca.pwdb.PropertyValue.StringStore} storage for unprotected values and + * {@link org.linguafranca.pwdb.PropertyValue.SealedStore} for protected values. + */ public abstract class ProtectedDatabase, G extends Group, E extends Entry, I extends Icon> extends AbstractDatabase { - private final List protectedProperties = new ArrayList<>(); - private PropertyValue.Strategy valueStrategy; + @SuppressWarnings("ArraysAsListWithZeroOrOneArgument") + private final List protectedProperties = new ArrayList<>(Arrays.asList(Entry.STANDARD_PROPERTY_NAME_PASSWORD)); + private PropertyValue.Strategy valueStrategy = new PropertyValue.Strategy() { + @Override + public PropertyValue.Factory getUnprotectectedValueFactory() { + return new PropertyValue.StringStore.Factory(); + } + + @Override + public PropertyValue.Factory getProtectectedValueFactory() { + return new PropertyValue.SealedStore.Factory(); + } + }; @Override public boolean shouldProtect(String propertyName){ diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java index 05b9c441..e4e61702 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java @@ -85,21 +85,18 @@ public static class Association { public static class StringProperty { - public StringProperty() { - - } - - public StringProperty(String key, Value value) { - this.key = key; - this.value = value; - } - @JacksonXmlProperty(localName = "Key") String key; @JacksonXmlProperty(localName = "Value") Value value; + public StringProperty() {} + public StringProperty(String key, Value value) { + this.key = key; + this.value = value; + } + public String getKey() { return key; } @@ -168,12 +165,10 @@ public void setProtected(Boolean _protected) { this._protected = _protected; } - public Boolean getProtectInMemory() { return protectInMemory; } - public void setProtectInMemory(Boolean protectInMemory) { this.protectInMemory = protectInMemory; } From e98dbf245394d39dc64a8f76734488272266c804 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 10 Sep 2024 16:38:42 +0100 Subject: [PATCH 182/228] Implement PropertyValue for Jackson and start adding tests --- .../java/org/linguafranca/pwdb/Entry.java | 8 +- .../org/linguafranca/pwdb/PropertyValue.java | 70 +++++++++++++-- .../pwdb/base/AbstractDatabase.java | 3 +- .../linguafranca/pwdb/base/AbstractEntry.java | 12 +-- .../pwdb/protect/ProtectedDatabase.java | 24 ++--- .../linguafranca/pwdb/PropertyValueTest.java | 39 +++++++- .../pwdb/kdbx/dom/DomEntryWrapper.java | 2 +- .../pwdb/kdbx/jackson/JacksonDatabase.java | 4 +- .../pwdb/kdbx/jackson/JacksonEntry.java | 42 +++++++-- .../jackson/JacksonSerializableDatabase.java | 50 +---------- .../jackson/converter/ValueDeserializer.java | 50 +++++------ .../jackson/converter/ValueSerializer.java | 30 +++---- .../pwdb/kdbx/jackson/model/EntryClasses.java | 78 ++-------------- .../pwdb/JacksonPropertyValueTest.java | 36 ++++++++ .../pwdb/kdbx/jaxb/JaxbEntry.java | 2 +- .../org/linguafranca/pwdb/kdb/KdbEntry.java | 2 +- .../pwdb/kdbx/simple/SimpleEntry.java | 2 +- .../kdbx/simple/SimplePropertyValueTest.java | 35 ++++++++ .../pwdb/checks/BasicDatabaseChecks.java | 16 ++-- .../pwdb/checks/PropertyValueChecks.java | 88 +++++++++++++++++++ .../pwdb/checks/SaveAndReloadChecks.java | 4 +- 21 files changed, 370 insertions(+), 227 deletions(-) create mode 100644 jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java create mode 100644 simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimplePropertyValueTest.java create mode 100644 test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java diff --git a/database/src/main/java/org/linguafranca/pwdb/Entry.java b/database/src/main/java/org/linguafranca/pwdb/Entry.java index 7c7d5818..1818eb28 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Entry.java +++ b/database/src/main/java/org/linguafranca/pwdb/Entry.java @@ -119,7 +119,7 @@ interface Matcher { /** * Sets the value of a property. Use of this method is not recommended for fields with protected values. - * Use {@link #setProperty(String, PropertyValue)} + * Use {@link #setPropertyValue(String, PropertyValue)} * *

    Other than the {@link #STANDARD_PROPERTY_NAMES} support for this method is optional. * @@ -129,7 +129,7 @@ interface Matcher { * non-standard properties are not supported * @see Database#supportsNonStandardPropertyNames() */ - void setProperty(String name, String value); + void setPropertyValue(String name, String value); /** * Sets the value of a property as a property value. The method does not check whether the @@ -143,7 +143,7 @@ interface Matcher { * non-standard properties are not supported * @see Database#supportsNonStandardPropertyNames() */ - void setProperty(String name, PropertyValue value); + void setPropertyValue(String name, PropertyValue value); /** * Removes this non-standard property, if it exists. @@ -258,7 +258,7 @@ interface Matcher { *

    Implementations should Touch LastModifiedTime when this method is called. * * @param pass a password - * @deprecated use {@link #setProperty(String, PropertyValue)} + * @deprecated use {@link #setPropertyValue(String, PropertyValue)} */ @Deprecated void setPassword(String pass); diff --git a/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java b/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java index a5ebf87d..8c078747 100644 --- a/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java +++ b/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java @@ -9,6 +9,9 @@ import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.security.Key; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; /** * An interface through which (textual) property values can be stored in memory as something other than String @@ -41,8 +44,49 @@ interface Factory { * A specification of which factories are to be used for unprotected values as opposed to protected values. */ interface Strategy { - PropertyValue.Factory getUnprotectectedValueFactory (); - PropertyValue.Factory getProtectectedValueFactory (); + /** + * A list of the properties that should be protected by default + */ + List getProtectedProperties(); + + /** + * Whether to throw and illegalAccess Exception if an attempt is made to read a protected value as a String + */ + boolean preventProtectedAccessAsString(); + + /** + * A factory for protected properties + */ + PropertyValue.Factory newProtected(); + + /** + * A factory for unprotected property values + */ + PropertyValue.Factory newUnprotected(); + + class Default implements Strategy { + + @Override + public List getProtectedProperties() { + //noinspection ArraysAsListWithZeroOrOneArgument + return new ArrayList<>(Arrays.asList(Entry.STANDARD_PROPERTY_NAME_PASSWORD)); + } + + @Override + public boolean preventProtectedAccessAsString() { + return false; + } + + @Override + public Factory newProtected() { + return new PropertyValue.SealedStore.Factory(); + } + + @Override + public Factory newUnprotected() { + return new PropertyValue.StringStore.Factory(); + } + } } /** @@ -138,7 +182,11 @@ public CharsStore(char [] value) { } public CharsStore(byte [] value) { - this.value = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(value)).array(); + ByteBuffer bb = ByteBuffer.wrap(value); + CharBuffer cb = StandardCharsets.UTF_8.decode(bb); + char[] chars = new char[cb.limit()]; + cb.get(chars); + this.value = chars; } @Override @@ -158,7 +206,11 @@ public CharSequence getValue() { @Override public byte [] getValueAsBytes() { - return StandardCharsets.UTF_8.encode(CharBuffer.wrap(this.value)).array(); + CharBuffer cb = CharBuffer.wrap(this.value); + ByteBuffer bb = StandardCharsets.UTF_8.encode(cb); + byte[] result = new byte[bb.limit()]; + bb.get(result); + return result; } @Override @@ -244,7 +296,7 @@ private Key retrieveKey(ByteBuffer buffer) { } } - private CharsStore getAsDefault() { + public CharsStore getAsCharsStore() { try { return ((CharsStore) sealedObject.getObject(retrieveKey(buffer))); } catch (Exception e) { @@ -253,22 +305,22 @@ private CharsStore getAsDefault() { } @Override public String getValueAsString() { - return getAsDefault().getValueAsString(); + return getAsCharsStore().getValueAsString(); } @Override public char[] getValueAsChars() { - return getAsDefault().getValueAsChars(); + return getAsCharsStore().getValueAsChars(); } @Override public byte[] getValueAsBytes() { - return getAsDefault().getValueAsBytes(); + return getAsCharsStore().getValueAsBytes(); } @Override public CharSequence getValue() { - return getAsDefault().getValue(); + return getAsCharsStore().getValue(); } @Override diff --git a/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java b/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java index b0ef0e60..7e17721c 100644 --- a/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java +++ b/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java @@ -19,7 +19,6 @@ import org.linguafranca.pwdb.*; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.UUID; @@ -108,7 +107,7 @@ public E newEntry(Entry entry) { for (String propertyName: entry.getPropertyNames()) { try { // all implementations must support setting of STANDARD_PROPERTY_NAMES - result.setProperty(propertyName, entry.getProperty(propertyName)); + result.setPropertyValue(propertyName, entry.getProperty(propertyName)); } catch (UnsupportedOperationException e) { // oh well, we tried } diff --git a/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java b/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java index e71186f2..50987d42 100644 --- a/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java +++ b/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java @@ -76,7 +76,7 @@ public String getUsername() { @Override public void setUsername(String username) { - setProperty(STANDARD_PROPERTY_NAME_USER_NAME, username); + setPropertyValue(STANDARD_PROPERTY_NAME_USER_NAME, username); touch(); } @@ -87,7 +87,7 @@ public String getPassword() { @Override public void setPassword(String pass) { - setProperty(STANDARD_PROPERTY_NAME_PASSWORD, pass); + setPropertyValue(STANDARD_PROPERTY_NAME_PASSWORD, pass); touch(); } @@ -98,7 +98,7 @@ public String getUrl() { @Override public void setUrl(String url) { - setProperty(STANDARD_PROPERTY_NAME_URL, url); + setPropertyValue(STANDARD_PROPERTY_NAME_URL, url); touch(); } @@ -109,7 +109,7 @@ public String getTitle() { @Override public void setTitle(String title) { - setProperty(STANDARD_PROPERTY_NAME_TITLE, title); + setPropertyValue(STANDARD_PROPERTY_NAME_TITLE, title); touch(); } @@ -120,7 +120,7 @@ public String getNotes() { @Override public void setNotes(String notes) { - setProperty(STANDARD_PROPERTY_NAME_NOTES, notes); + setPropertyValue(STANDARD_PROPERTY_NAME_NOTES, notes); touch(); } @@ -130,7 +130,7 @@ public PropertyValue getPropertyValue(String name) { } @Override - public void setProperty(String name, PropertyValue value) { + public void setPropertyValue(String name, PropertyValue value) { throw new UnsupportedOperationException(); } diff --git a/database/src/main/java/org/linguafranca/pwdb/protect/ProtectedDatabase.java b/database/src/main/java/org/linguafranca/pwdb/protect/ProtectedDatabase.java index 1d9773e1..7c50e16c 100644 --- a/database/src/main/java/org/linguafranca/pwdb/protect/ProtectedDatabase.java +++ b/database/src/main/java/org/linguafranca/pwdb/protect/ProtectedDatabase.java @@ -3,8 +3,6 @@ import org.linguafranca.pwdb.*; import org.linguafranca.pwdb.base.AbstractDatabase; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -16,37 +14,25 @@ * {@link org.linguafranca.pwdb.PropertyValue.SealedStore} for protected values. */ public abstract class ProtectedDatabase, G extends Group, E extends Entry, I extends Icon> extends AbstractDatabase { - @SuppressWarnings("ArraysAsListWithZeroOrOneArgument") - private final List protectedProperties = new ArrayList<>(Arrays.asList(Entry.STANDARD_PROPERTY_NAME_PASSWORD)); - private PropertyValue.Strategy valueStrategy = new PropertyValue.Strategy() { - @Override - public PropertyValue.Factory getUnprotectectedValueFactory() { - return new PropertyValue.StringStore.Factory(); - } - - @Override - public PropertyValue.Factory getProtectectedValueFactory() { - return new PropertyValue.SealedStore.Factory(); - } - }; + private PropertyValue.Strategy valueStrategy = new PropertyValue.Strategy.Default(); @Override public boolean shouldProtect(String propertyName){ - return protectedProperties.contains(propertyName); + return valueStrategy.getProtectedProperties().contains(propertyName); } @Override public void setShouldProtect(String propertyName, boolean protect){ if (protect) { - protectedProperties.add(propertyName); + valueStrategy.getProtectedProperties().add(propertyName); } else { - protectedProperties.remove(propertyName); + valueStrategy.getProtectedProperties().remove(propertyName); } } @Override public List listShouldProtect(){ - return Collections.unmodifiableList(protectedProperties); + return Collections.unmodifiableList(valueStrategy.getProtectedProperties()); } /** diff --git a/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java b/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java index 253072ae..947dac0d 100644 --- a/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java +++ b/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java @@ -11,7 +11,33 @@ public class PropertyValueTest { public static final String THIS_IS_A_SECRET = "This is a secret + לַחַיִּים"; + public static final String ANOTHER_SECRET = "password with accents àéç"; Logger logger = LoggerFactory.getLogger(PropertyValueTest.class); + + @Test + public void charsTest() { + PropertyValue.CharsStore.Factory factory = new PropertyValue.CharsStore.Factory(); + + // test as CharSequence + PropertyValue.CharsStore testValue = factory.of(THIS_IS_A_SECRET); + assertEquals(THIS_IS_A_SECRET, testValue.getValue().toString()); + assertEquals(THIS_IS_A_SECRET, testValue.getValueAsString()); + assertArrayEquals(THIS_IS_A_SECRET.toCharArray(), testValue.getValueAsChars()); + assertArrayEquals(THIS_IS_A_SECRET.getBytes(StandardCharsets.UTF_8), testValue.getValueAsBytes()); + // test as char[] + testValue = factory.of(THIS_IS_A_SECRET.toCharArray()); + assertEquals(THIS_IS_A_SECRET, testValue.getValue().toString()); + assertEquals(THIS_IS_A_SECRET, testValue.getValueAsString()); + assertArrayEquals(THIS_IS_A_SECRET.toCharArray(), testValue.getValueAsChars()); + assertArrayEquals(THIS_IS_A_SECRET.getBytes(StandardCharsets.UTF_8), testValue.getValueAsBytes()); + // test as byte[] + testValue = factory.of(THIS_IS_A_SECRET.getBytes()); + assertEquals(THIS_IS_A_SECRET, testValue.getValue().toString()); + assertEquals(THIS_IS_A_SECRET, testValue.getValueAsString()); + assertArrayEquals(THIS_IS_A_SECRET.toCharArray(), testValue.getValueAsChars()); + assertArrayEquals(THIS_IS_A_SECRET.getBytes(StandardCharsets.UTF_8), testValue.getValueAsBytes()); + } + @Test public void sealedObjectTest() { PropertyValue.CharsStore.Factory factory = new PropertyValue.CharsStore.Factory(); @@ -36,7 +62,7 @@ public void sealedObjectTest() { assertEquals(testValue.getValueAsString(), sealed.getValueAsString()); } @Test - public void NoOpTest() { + public void stringTest() { PropertyValue.StringStore.Factory factory = new PropertyValue.StringStore.Factory(); PropertyValue.StringStore testValue = factory.of(THIS_IS_A_SECRET); @@ -46,4 +72,15 @@ public void NoOpTest() { testValue = factory.of(THIS_IS_A_SECRET.getBytes(StandardCharsets.UTF_8)); assertEquals(THIS_IS_A_SECRET, testValue.getValueAsString()); } + + @Test + public void sealedScriptTest(){ + PropertyValue.SealedStore sealed = new PropertyValue.SealedStore.Factory().of(ANOTHER_SECRET); + PropertyValue.CharsStore charStore = sealed.getAsCharsStore(); + byte[] bytes1 = charStore.getValueAsBytes(); + byte[] bytes = sealed.getValueAsBytes(); + byte[] answer = ANOTHER_SECRET.getBytes(StandardCharsets.UTF_8); + assertArrayEquals(ANOTHER_SECRET.getBytes(StandardCharsets.UTF_8), bytes); + + } } \ No newline at end of file diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java index 39575301..293b41c8 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java @@ -68,7 +68,7 @@ public String getProperty(String name) { } @Override - public void setProperty(String name, String value) { + public void setPropertyValue(String name, String value) { Element property = DomHelper.getElement(String.format(DomHelper.PROPERTY_ELEMENT_FORMAT, name), element, false); if (property == null) { property = DomHelper.newElement("String", element); diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java index ef99e038..84ae3a13 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java @@ -19,10 +19,10 @@ import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.StreamConfiguration; import org.linguafranca.pwdb.StreamFormat; -import org.linguafranca.pwdb.base.AbstractDatabase; import org.linguafranca.pwdb.kdbx.KdbxHeader; import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import org.linguafranca.pwdb.kdbx.jackson.model.KeePassFile; +import org.linguafranca.pwdb.protect.ProtectedDatabase; import java.io.IOException; import java.io.InputStream; @@ -34,7 +34,7 @@ import static org.linguafranca.pwdb.kdbx.jackson.JacksonSerializableDatabase.createEmptyDatabase; -public class JacksonDatabase extends AbstractDatabase { +public class JacksonDatabase extends ProtectedDatabase { KeePassFile keePassFile; StreamFormat streamFormat; diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java index 92bee42e..6bd19d70 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java @@ -23,6 +23,7 @@ import org.jetbrains.annotations.NotNull; +import org.linguafranca.pwdb.PropertyValue; import org.linguafranca.pwdb.base.AbstractEntry; import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.jackson.converter.Base64ToUUIDConverter; @@ -128,7 +129,7 @@ public static JacksonEntry createEntry(JacksonDatabase database) { result.parent = null; // avoiding setProperty as it does a touch(); for (String p : STANDARD_PROPERTY_NAMES) { - result.string.add(new StringProperty(p, new StringProperty.Value(""))); + result.string.add(new StringProperty(p, database.getPropertyValueStrategy().newUnprotected().of(""))); } return result; } @@ -136,17 +137,44 @@ public static JacksonEntry createEntry(JacksonDatabase database) { @Override @JsonIgnore public String getProperty(String s) { - return getStringContent(getStringProperty(s, string)); + StringProperty sp = getStringProperty(s, string); + if (sp == null) { + return null; + } + if (sp.getValue().isProtected() && database.getPropertyValueStrategy().preventProtectedAccessAsString()) { + throw new IllegalStateException("Access to protected property as String is prevented by strategy"); + } + return sp.getValue().getValueAsString(); } @Override @JsonIgnore - public void setProperty(String s, String s1) { - StringProperty sp; - if ((sp = getStringProperty(s, string)) != null) { - this.string.remove(sp); + public void setPropertyValue(String s, String s1) { + StringProperty sp = getStringProperty(s, string); + if (sp != null) { + sp.setValue(database.getPropertyValueStrategy().newUnprotected().of(s1)); + return; + } + string.add(new StringProperty(s, database.getPropertyValueStrategy().newUnprotected().of(s1))); + touch(); + } + + @Override + @JsonIgnore + public PropertyValue getPropertyValue(String name) { + StringProperty sp = getStringProperty(name, string); + return sp != null? sp.getValue() : null; + } + + @Override + @JsonIgnore + public void setPropertyValue(String name, PropertyValue value) { + StringProperty sp = getStringProperty(name, string); + if (sp != null) { + sp.setValue(value); + return; } - this.string.add(new StringProperty(s, new StringProperty.Value(s1))); + string.add(new StringProperty(name, value)); touch(); } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java index 5aedec57..7cb11432 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java @@ -22,13 +22,11 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; -import org.jetbrains.annotations.NotNull; -import org.linguafranca.pwdb.Entry; +import org.linguafranca.pwdb.PropertyValue; import org.linguafranca.pwdb.SerializableDatabase; import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.jackson.converter.ValueDeserializer; import org.linguafranca.pwdb.kdbx.jackson.converter.ValueSerializer; -import org.linguafranca.pwdb.kdbx.jackson.model.EntryClasses; import org.linguafranca.pwdb.kdbx.jackson.model.KeePassFile; import org.linguafranca.pwdb.security.StreamEncryptor; @@ -38,8 +36,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; public class JacksonSerializableDatabase implements SerializableDatabase { @@ -67,7 +63,7 @@ public JacksonSerializableDatabase(KeePassFile keePassFile) { public JacksonSerializableDatabase load(InputStream inputStream) throws IOException { XmlMapper mapper = new XmlMapper(); SimpleModule module = new SimpleModule(); - module.addDeserializer(EntryClasses.StringProperty.Value.class, new ValueDeserializer(encryptor)); + module.addDeserializer(PropertyValue.class, new ValueDeserializer(encryptor, new PropertyValue.Strategy.Default())); mapper.registerModule(module); keePassFile = mapper.readValue(inputStream, KeePassFile.class); return this; @@ -76,11 +72,10 @@ public JacksonSerializableDatabase load(InputStream inputStream) throws IOExcept @Override public void save(OutputStream outputStream) { - prepareForSave(keePassFile.root.group); try { SimpleModule module = new SimpleModule(); - module.addSerializer(EntryClasses.StringProperty.Value.class, new ValueSerializer(encryptor)); + module.addSerializer(PropertyValue.class, new ValueSerializer(encryptor)); // disable auto-detection, only use annotated values XmlMapper mapper = XmlMapper.builder() .disable(MapperFeature.AUTO_DETECT_CREATORS, @@ -119,45 +114,6 @@ public void save(OutputStream outputStream) { } } - /** - * Create a list of names of properties that should be encrypted by default - */ - @NotNull - private List getToEncrypt() { - final List toEncrypt = new ArrayList<>(); - for (String propertyName: Entry.STANDARD_PROPERTY_NAMES) { - if (keePassFile.meta.memoryProtection.shouldProtect(propertyName)) { - toEncrypt.add(propertyName); - } - } - return toEncrypt; - } - - /** - * Utility to mark fields that need to be encrypted and vice versa - * - * @param parent the group to start from - */ - private static void prepareForSave(JacksonGroup parent){ - for (JacksonGroup group: parent.groups) { - prepareForSave(group); - } - for (JacksonEntry entry: parent.entries) { - for (EntryClasses.StringProperty property : entry.string) { - boolean shouldProtect = parent.database.shouldProtect(property.getKey()); - property.getValue().setProtectOnOutput(shouldProtect || property.getValue().getProtectOnOutput()); - } - if (Objects.nonNull(entry.history)) { - for (JacksonEntry entry2 : entry.history.getEntry()) { - for (EntryClasses.StringProperty property : entry2.string) { - boolean shouldProtect = parent.database.shouldProtect(property.getKey()); - property.getValue().setProtectOnOutput(shouldProtect || property.getValue().getProtectOnOutput()); - } - } - } - } - } - @Override public byte[] getHeaderHash() { return keePassFile.meta.headerHash; diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java index 91fc3330..3c9639b4 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java @@ -20,6 +20,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import org.apache.commons.codec.binary.Base64; +import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.PropertyValue; import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.jackson.model.EntryClasses; import org.linguafranca.pwdb.security.StreamEncryptor; @@ -27,53 +29,43 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; -public class ValueDeserializer extends StdDeserializer { +public class ValueDeserializer extends StdDeserializer { private final StreamEncryptor encryptor; + private final PropertyValue.Strategy strategy; - public ValueDeserializer(StreamEncryptor encryptor) { + public ValueDeserializer(StreamEncryptor encryptor, PropertyValue.Strategy strategy) { super(ValueDeserializer.class); this.encryptor = encryptor; + this.strategy = strategy; } @Override - public EntryClasses.StringProperty.Value deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + public PropertyValue deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { JsonNode node = p.getCodec().readTree(p); - EntryClasses.StringProperty.Value result = new EntryClasses.StringProperty.Value(); - if (node.isTextual()) { - result.setText(node.textValue()); - return result; + return strategy.newUnprotected().of(node.textValue()); } + if (node.isObject()) { // TODO not clear what is happening here, looks like it's not exactly correct //We need to decrypt all Protected values - if (node.has("Protected") && Boolean.TRUE.equals(Helpers.toBoolean(node.get("Protected").asText()))) { - if (node.has("")) { - String cipherText = node.get("").asText(); - if (cipherText != null && !cipherText.isEmpty()) { - - //Decode to byte the Base64 text - byte[] encrypted = Base64.decodeBase64(cipherText.getBytes()); - String decrypted = new String(encryptor.decrypt(encrypted), StandardCharsets.UTF_8); - result.setText(decrypted); - result.setProtectOnOutput(true); - } - } - } else { - //If an element is not marked us Protected we need to copy the value as is - if (node.has("ProtectInMemory")) { - Boolean protectInMemory = Helpers.toBoolean(node.get("ProtectInMemory").asText()); - result.setProtectInMemory(protectInMemory); + String cipherText = ""; + if (node.has("")) { + cipherText = node.get("").asText(); + } - if (node.has("")) { - result.setText(node.get("").asText()); - } - } + if (node.has("Protected") && Boolean.TRUE.equals(Helpers.toBoolean(node.get("Protected").asText()))) { + //Decode to byte the Base64 text + byte[] encrypted = Base64.decodeBase64(cipherText.getBytes()); + byte[] decrypted = encryptor.decrypt(encrypted); + return strategy.newProtected().of(decrypted); } + return strategy.newUnprotected().of(cipherText); } - return result; + + throw new IllegalStateException("Error parsing XML node type is " + node.getClass()); } } \ No newline at end of file diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java index a6c8fc81..99049796 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java @@ -20,14 +20,13 @@ import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; import org.apache.commons.codec.binary.Base64; -import org.linguafranca.pwdb.kdbx.jackson.model.EntryClasses; -import org.linguafranca.pwdb.kdbx.jackson.model.EntryClasses.StringProperty.Value; +import org.linguafranca.pwdb.PropertyValue; import org.linguafranca.pwdb.security.StreamEncryptor; import java.io.IOException; -public class ValueSerializer extends StdSerializer { +public class ValueSerializer extends StdSerializer { private final StreamEncryptor encryptor; @@ -36,28 +35,27 @@ public ValueSerializer(StreamEncryptor encryptor) { this.encryptor = encryptor; } + private String encrypt(byte[] bytes) { + //Cipher + byte[] encrypted = encryptor.encrypt(bytes); + //Convert to base64 + return new String(Base64.encodeBase64(encrypted)); + } @Override - public void serialize(Value value, JsonGenerator gen, SerializerProvider provider) throws IOException { + public void serialize(PropertyValue value, JsonGenerator gen, SerializerProvider provider) throws IOException { final ToXmlGenerator xmlGenerator = (ToXmlGenerator) gen; xmlGenerator.writeStartObject(); - String stringToWrite = value.getText(); - //We need to encrypt and convert to base64 every protected element - if (value.getProtectOnOutput()) { + String stringToWrite = value.isProtected() ? + encrypt(value.getValueAsBytes()) : + value.getValueAsString(); + + if (value.isProtected()) { xmlGenerator.setNextIsAttribute(true); xmlGenerator.writeStringField("Protected", "True"); - String plain = value.getText(); - if (plain == null) { - plain = ""; - } - //Cipher - byte[] encrypted = encryptor.encrypt(plain.getBytes()); - //Convert to base64 - stringToWrite = new String(Base64.encodeBase64(encrypted)); } - xmlGenerator.setNextIsAttribute(false); xmlGenerator.setNextIsUnwrapped(true); xmlGenerator.writeStringField("text", stringToWrite); diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java index e4e61702..c768d4eb 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java @@ -18,8 +18,8 @@ import java.util.ArrayList; import java.util.List; -import java.util.Objects; +import org.linguafranca.pwdb.PropertyValue; import org.linguafranca.pwdb.kdbx.jackson.JacksonEntry; import org.linguafranca.pwdb.kdbx.jackson.converter.BooleanToStringConverter; import org.linguafranca.pwdb.kdbx.jackson.converter.StringToBooleanConverter; @@ -28,7 +28,6 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText; public abstract class EntryClasses { public static StringProperty getStringProperty(String name, List string) { @@ -40,10 +39,6 @@ public static StringProperty getStringProperty(String name, List return null; } - public static String getStringContent(StringProperty property) { - return property == null || property.value == null ? null : property.value.text; - } - public static BinaryProperty getBinaryProp(String name, List binary) { for (BinaryProperty property : binary) { if (property.key.equals(name)) { @@ -89,10 +84,12 @@ public static class StringProperty { String key; @JacksonXmlProperty(localName = "Value") - Value value; + PropertyValue value; + @SuppressWarnings("unused") public StringProperty() {} - public StringProperty(String key, Value value) { + + public StringProperty(String key, PropertyValue value) { this.key = key; this.value = value; } @@ -101,7 +98,7 @@ public String getKey() { return key; } - public Value getValue() { + public PropertyValue getValue() { return value; } @@ -109,70 +106,9 @@ public void setKey(String key) { this.key = key; } - public void setValue(Value value) { + public void setValue(PropertyValue value) { this.value = value; } - - public static class Value { - - public Value() { - } - - public Value(String text) { - this.text = text; - this._protected = false; - } - - public Value(String text, Boolean _protected) { - this._protected = _protected; - this.text = text; - } - - @JacksonXmlProperty(localName = "ProtectInMemory", isAttribute = true) - protected Boolean protectInMemory; - - @JacksonXmlProperty(localName = "Protected", isAttribute = true) - Boolean _protected; - - - @JacksonXmlProperty(localName = "kpj2-ProtectOnOutput", isAttribute = true) - Boolean protectOnOutput; - - @JacksonXmlText - String text; - - public String getText() { - return text; - } - - public void setText(String text) { - this.text = text; - } - - public void setProtectOnOutput(boolean aProtected) { - this.protectOnOutput = aProtected; - } - - public boolean getProtectOnOutput() { - return Objects.nonNull(this.protectOnOutput) && this.protectOnOutput; - } - - public Boolean getProtected() { - return _protected; - } - - public void setProtected(Boolean _protected) { - this._protected = _protected; - } - - public Boolean getProtectInMemory() { - return protectInMemory; - } - - public void setProtectInMemory(Boolean protectInMemory) { - this.protectInMemory = protectInMemory; - } - } } public static class BinaryProperty { diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java new file mode 100644 index 00000000..c5b8275a --- /dev/null +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2024 Jo Rabin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb; + +import org.linguafranca.pwdb.checks.BasicDatabaseChecks; +import org.linguafranca.pwdb.checks.PropertyValueChecks; +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; + +import java.io.IOException; + +public class JacksonPropertyValueTest extends PropertyValueChecks { + + public JacksonPropertyValueTest() throws IOException { + super(true); + } + + + @Override + public Database createDatabase() throws IOException { + return new JacksonDatabase(); + } + +} diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java index 1eafe10d..1d4bea59 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java @@ -82,7 +82,7 @@ public String getProperty(String name) { } @Override - public void setProperty(String name, String value) { + public void setPropertyValue(String name, String value) { StringField toRemove = null; for (StringField field: delegate.getString()){ if (field.getKey().equals(name)) { diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java index ac9927b7..15c32115 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java @@ -62,7 +62,7 @@ public String getProperty(String name) { } @Override - public void setProperty(String name, String value) { + public void setPropertyValue(String name, String value) { switch (name) { case STANDARD_PROPERTY_NAME_USER_NAME: setUsername(value); break; case STANDARD_PROPERTY_NAME_PASSWORD: setPassword(value); break; diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java index 93f4f105..ff76f94d 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java @@ -106,7 +106,7 @@ public String getProperty(String s) { } @Override - public void setProperty(String s, String s1) { + public void setPropertyValue(String s, String s1) { EntryClasses.StringProperty sp; if ((sp = getStringProperty(s, string)) != null) { this.string.remove(sp); diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimplePropertyValueTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimplePropertyValueTest.java new file mode 100644 index 00000000..6ab4df5a --- /dev/null +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimplePropertyValueTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2024 Jo Rabin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb.kdbx.simple; + +import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.checks.PropertyValueChecks; + +import java.io.IOException; + +public class SimplePropertyValueTest extends PropertyValueChecks { + + public SimplePropertyValueTest() throws IOException { + super(false); + } + + + @Override + public Database createDatabase() throws IOException { + return new SimpleDatabase(); + } + +} diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java index dd0d06b5..02764f41 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java @@ -135,19 +135,19 @@ public void testSetFields () { assertEquals(e1.getIcon(), ic1); // databases have to support setting of standard properties - e1.setProperty(Entry.STANDARD_PROPERTY_NAME_TITLE, "A title"); + e1.setPropertyValue(Entry.STANDARD_PROPERTY_NAME_TITLE, "A title"); assertEquals("A title", e1.getTitle()); - e1.setProperty(Entry.STANDARD_PROPERTY_NAME_USER_NAME, "username"); + e1.setPropertyValue(Entry.STANDARD_PROPERTY_NAME_USER_NAME, "username"); assertEquals("username", e1.getUsername()); - e1.setProperty(Entry.STANDARD_PROPERTY_NAME_NOTES, "notes"); + e1.setPropertyValue(Entry.STANDARD_PROPERTY_NAME_NOTES, "notes"); assertEquals("notes", e1.getNotes()); - e1.setProperty(Entry.STANDARD_PROPERTY_NAME_PASSWORD, "password"); + e1.setPropertyValue(Entry.STANDARD_PROPERTY_NAME_PASSWORD, "password"); assertEquals("password", e1.getPassword()); - e1.setProperty(Entry.STANDARD_PROPERTY_NAME_URL, "url"); + e1.setPropertyValue(Entry.STANDARD_PROPERTY_NAME_URL, "url"); assertEquals("url", e1.getUrl()); try { - e1.setProperty("silly", "hello"); + e1.setPropertyValue("silly", "hello"); assertEquals("hello", e1.getProperty("silly")); List properties = new ArrayList<>(Entry.STANDARD_PROPERTY_NAMES); properties.add("silly"); @@ -194,7 +194,7 @@ public void checkAddChangeRemoveProperty() { E entry = database.newEntry(); assertEquals(Entry.STANDARD_PROPERTY_NAMES.size(), entry.getPropertyNames().size()); try { - entry.setProperty("test", "test1"); + entry.setPropertyValue("test", "test1"); } catch (UnsupportedOperationException e) { if (!database.supportsNonStandardPropertyNames()) { return; @@ -202,7 +202,7 @@ public void checkAddChangeRemoveProperty() { fail("Database must report that it doesn't support non standrad properties"); } assertEquals("test1", entry.getProperty("test")); - entry.setProperty("test", "test2"); + entry.setPropertyValue("test", "test2"); assertEquals("test2", entry.getProperty("test")); assertTrue(entry.removeProperty("test")); assertFalse(entry.removeProperty("test")); diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java new file mode 100644 index 00000000..19727d07 --- /dev/null +++ b/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java @@ -0,0 +1,88 @@ +/* + * Copyright 2015 Jo Rabin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.checks; + +import org.junit.Test; +import org.linguafranca.pwdb.*; + +import java.io.IOException; +import java.util.function.Consumer; + +import static org.junit.Assert.*; +import static org.junit.Assume.assumeFalse; + +/** + * Testing the operation of PropertyValue mechanisms + */ +public abstract class PropertyValueChecks, G extends Group, + E extends Entry, I extends Icon> { + + private final boolean propertyValueSupported; + protected D database; + + public PropertyValueChecks(boolean propertyValueSupported) throws IOException { + this.database = createDatabase(); + this.propertyValueSupported = propertyValueSupported; + } + + public abstract D createDatabase() throws IOException; + + /** + * Catches expected thrown exception + * + * @param f a Consumer i.e. a function that does have output + * @param c the exception that is expected to be thrown when the function is executed + */ + private void catchException(Consumer f, Class c) { + try { + f.accept(null); + } catch (Exception e) { + if (c.equals(e.getClass())) { + return; + } + } + fail(c.getName() + " expected"); + } + + /** + * Check that unsupported operations throw exceptions + */ + @Test + public void testNoPropertyValueSupport() { + assumeFalse(propertyValueSupported); + + assertFalse(database.supportsPropertyValueStrategy()); + // test a sample of the methods that are supposed to throw if used here + catchException((z) -> database.setPropertyValueStrategy(null), UnsupportedOperationException.class); + catchException((z) -> database.getPropertyValueStrategy(), UnsupportedOperationException.class); + catchException((z) -> database.setShouldProtect(null, true), UnsupportedOperationException.class); + catchException((z) -> database.listShouldProtect(), UnsupportedOperationException.class); + + E entry = database.newEntry(); + catchException((z) -> entry.getPropertyValue(null), UnsupportedOperationException.class); + catchException((z) -> entry.setPropertyValue(null, (PropertyValue) null), UnsupportedOperationException.class); + + // these are the defaults established by createDatabase() + assertTrue(database.shouldProtect(Entry.STANDARD_PROPERTY_NAME_PASSWORD)); + assertFalse(database.shouldProtect(Entry.STANDARD_PROPERTY_NAME_USER_NAME)); + assertFalse(database.shouldProtect(Entry.STANDARD_PROPERTY_NAME_NOTES)); + assertFalse(database.shouldProtect(Entry.STANDARD_PROPERTY_NAME_URL)); + assertFalse(database.shouldProtect(Entry.STANDARD_PROPERTY_NAME_TITLE)); + } + + +} diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java index 82c4733f..4e45886b 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java @@ -265,9 +265,9 @@ public void testNewDatabase() throws IOException { assertEquals("A new entry", entry1.getTitle()); entry1.setUsername("user name"); assertEquals("user name", entry1.getUsername()); - entry1.setProperty("random", "new"); + entry1.setPropertyValue("random", "new"); assertEquals("new", entry1.getProperty("random")); - entry1.setProperty("random", "old"); + entry1.setPropertyValue("random", "old"); assertEquals("old", entry1.getProperty("random")); From 90485c686fb1ba34aee5e19fdc2d78e0649c5fa3 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 10 Sep 2024 19:06:36 +0100 Subject: [PATCH 183/228] Extensive documentation updates --- PropertyValueProtection.md | 220 +++++++++++++++++++++++++++++-------- readme.md | 104 +++++++++--------- 2 files changed, 225 insertions(+), 99 deletions(-) diff --git a/PropertyValueProtection.md b/PropertyValueProtection.md index a1932c58..4da67d55 100644 --- a/PropertyValueProtection.md +++ b/PropertyValueProtection.md @@ -1,48 +1,178 @@ -Properties that have sensitive values are called "Protected Properties". Up to -version KeePassJava2 2.2, what is, and what is not, a protected property is determined -by the database that is loaded from file, which sets protection on a property -by property basis. - -The database also specifies that new Entries should have fields of -specified names should be protected by default, typically this is restricted to -the `password` property. Up to version 2.2 new databases followed this convention -by setting `password` as the only property protected by default. - -In the KeePass format, protected properties are encrypted in addition to the -encryption applied to the database as a whole when saved as a KeePass file. - -Up to version 2.2 of KeePassJava2 protected property values have been held as -unencrypted Strings in memory and have been accessed as Strings using the -`String getProperty(String)` and `String getPassword(String)` methods. - -From version 2.3 of KeePassJava2, the storage of property values is controlled by the -`PropertyValue` interface. Users of KeePassJava2 may set implementations -of `PropertyValue` to be used for unprotected and protected properties -so that they may have control over how those property values are stored in memory. - -The method - -`void Database.setPropertyValueStrategy(PropertyValue.Builder unprotectedPropertyBuilder, - PropertyValue.Builder protectedPropertyBuilder);` - -is used to set the strategy. By default `PropertyValue.Default` and `PropertyValue.Protected` -are set as the means of storage of values. - -New accessors for property values are provided in the `Entry` class. - -`PropertyValue getPropertyValue(String)` and `void setPropertyValue(String, PropertyValue)` -are intended as the primary means of access to property values. - -`String getPropertyValue(String)` and `void setPropertyValue(String, String)` -are now deprecated. `getPropertyValue` continues to retrieve both protected -and unprotected values as String. `setPropertyValue` stores values as either -protected or unprotected according to whether `Database.shouldProtect` returns -true or false. - -A new method `Database.setProtectByDefault` is added to control whether -properties of a particular name will be protected or not. Calling this method -after a database is loaded causes the protection of all fields of all entries -affected to be updated to use the strategy defined on the Database. +# Property Values + +## Protected Properties + +KeePass allows distinguishing more sensitive string valued properties from less sensitive. Properties +that have sensitive values are called "Protected Properties". + +KDBX files contain a list of the standard properties and whether they are to be treated as protected +by default. The standard properties include password, which is so treated. There doesn't seem to be a way of updating +this list in the Windows KeePass implementation, and in any case documentation says that the +list is updated to default after load of a database, so it seems that it ignored. + +Individual properties can be marked as "protect in memory", but there is no way to indicate that +all properties of that name should be protected. + +## KeePass (Windows) Implementation of Protected Properties + +What KeePass means by property protection is firstly that they are additionally encrypted inside the +overall encryption of the database file, this is so that when they are loaded as unencrypted XML +they are not exposed within the process memory. Instead, they are decrypted and immediately stored in a +secure way (DPAPI in Windows) so they can't be spied upon. +See [KeePass documentation in Inner Encyption](https://keepass.info/help/kb/kdbx.html#ienc ) and +[Process Memory Protection](https://keepass.info/help/base/security.html#secmemprot). + +In summary, they are stored in an additionally secure way (additional to file security) in XML, +and they are stored in a secure way in memory. + +## KeePassJava2 Approach to Protected Properties + +### Up to KeePassJava2 2.2.2 + +All property values are stored as Strings in process memory, meaning that they can be seen via process dumps +and also, worse, Java Strings are immutable and this means that passwords stored as Strings +end up in the String pool, can't be zeroed and will be garbage collected only when they are. + +I don't regard this is a *terrible* problem, but you don't have to try hard find loads +of discussion about how *awful* it is on Google, and for sure, it's certainly not as good as it could +be for more security conscious applications, or applications where the infrastructure +it runs on is more vulnerable. + +### Alternatives to Storing Passwords as Strings + +You are probably at least as good as Google searches as me, good chance you may be better. So you] +can read all about this there. + +#### When transferring + +For a start, storing the value as anything other than a String improves the situation. However, +this needs to be accompanied by providing a way of setting the value other than using a String, +and retrieving it other than by using a String. So the first step is to enhance the +property accessor methods of the `Entry` interface to allow provision and retrieval of sensitive +values using `byte[]`, `char[]` and `CharSequence`. To be clear, the accessor methods provide the +sensitive data in a non-obfuscated form, and it's up to the caller to minimise the time +the fields contain that data before being cleared. + +Note that if the provider or consumer of a password does so as a String then you're out of +luck trying to avoid strings at all. Likewise, if you collect passwords from a dialog box. + +#### At rest + +So, as to how to store protected data "at rest" in the application? As noted, using anything other +than a String improves the situation. Obfuscation of the data using a hash may improve it +further, but a determined attacker will know (or might be able to find) the hash key anyway. + +Storing the data in an encrypted form could be attractive. Leaving it in the encrypted form that +it appears in the KDBX InnerStream is not practical, as this stream encrypted approach +depends on the encrypted property values appearing in the same order for encryption and decryption, and +it would not be practical to encrypt/decrypt all protected fields when accessing or manipulating +any one of them. + +## KeePassJava2 2.2.3 Property Value Strategy + +In the end it's up to the user of the library to decide what is the right approach to the +trade-off between vulnerability, risk and increased resource consumption. + +### Retrieval and Storage of Property Values + +Because of this, KeePassJava2 provides an interface for the storage and retrieval of property values. +```Java +public interface PropertyValue { + CharSequence getValue(); + + char[] getValueAsChars(); + + byte[] getValueAsBytes(); + + boolean isProtected(); + + String getValueAsString(); + // ... +} +``` +It also provides an interface for the creation of values: + +```Java +interface Factory

    { + P of (CharSequence aCharSequence); + + P of (char [] value); + + P of (byte [] value); +} +``` +which is an inner class of `Property Value`. Storage implementations will +implement `PropertyValue` and will implement a `Factory` for their creation. They may wish +to provide creation of the Factory as a static member on the class being created e.g. + +```Java +public class SpecialPropertyValue { + public static Factory getFactory(); + // ... +} + +``` +### Strategy Class + +KeePassJava2 provides a "strategy" to determine how it stores values. It does not enforce the strategy +so a caller is free to create a protected value where one is not called for by default, or use +any implementation or strategy to store a value. + +On save of the database, property values are saved to the inner stream as protected if the +`isProtected()` method of their implementation returns `true`. On reload, they will be stored +using the strategy defined default class for whether they are protected or not. When a database is +reloaded the default protected properties when it was saved will not be reloaded, as there's nowhere +in KDBX to store this information in a standardized way. (True, it could be stored in a non-standard way.) + +Changing the strategy doesn't alter the way that existing values are stored in the database. + +```Java +interface Strategy { + /** + * A list of the properties that should be protected by default + */ + List getProtectedProperties(); + + /** + * A factory for protected property values + */ + PropertyValue.Factory newProtected(); + + /** + * A factory for unprotected property values + */ + PropertyValue.Factory newUnprotected(); + + /** + * Return a factory given a property name and the properties that should be protected + */ + default PropertyValue.Factory getFactoryFor(String propertyName) { + return getProtectedProperties().contains(propertyName) ? + newProtected() : + newUnprotected(); + } +} +``` +#### Default Implementations of PropertyValue +There are three default implementations of `PropertyValue`: +- `PropertyValue.StringStore` stores values as strings +- `PropertyValue.CharsStore` stores values as char[] +- `PropertyValue.SealedStore` stores values as `javax.crypto.SealedObject` and stores the key using a `ByteBuffer` +obtained using the `ByteBuffer.allocateDirect()` method. +#### Default Implementation of Strategy +- `PropertyValue.Strategy.Default` defines `passowrd` as the only protected value and `CharsStore` and +`SealedStore` as the unprotected and protected `PropertyValue` implementations. + +### Implementation in Databases +From KeepassJava2 2.2.3 the Jackson implementation supports setting and getting of `PropertyValue`s +from an `Entry`. + +It supports setting and getting of `Strategy` from `Database`. + +If `Database.supportsPropertyValueStrategy()` returns false, then attempts to use any methods associated with PropertyValue from other database implementations +cause an `UnsupportedOperationException` to be raised. + + diff --git a/readme.md b/readme.md index 96aedd71..e43e51dd 100644 --- a/readme.md +++ b/readme.md @@ -16,7 +16,7 @@ be just the things for you. Features to date: -- Read and write KeePass 2.x format (File formats V3 and V4) +- Read and write KeePass 2.x format (KDBX file formats V3 and V4) - Keepass 2.x Password and Keyfile Credentials - Read KeePass 1.x format (Rijndael only) - *No* requirement for JCE Policy Files @@ -39,16 +39,14 @@ It is licensed under the Apache 2 License and is currently usable. ## Current Status -After a period of neglect, the project is (May 2023) back in development. - -The current code is version 2.2.2. This is on the main branch. See [Build from Source](#build-from-source) +The current code is version 2.2.2 - released to Maven September 2024. This is on the main branch. See [Build from Source](#build-from-source) Key updates relative to 2.1: - Java 8 (dependencies no longer support Java 7) - File format version 4 support - with Argon2 -- Inclusion of Jackson based KDBX support with a view to removing SimpleXML and JAXB support +- Inclusion of Jackson based KDBX support with a view to removing SimpleXML, JAXB and JAXB support - Updated keyfile support -- Updated dependencies to remove known vulnerabilities +- Updated dependencies See the [changelog](CHANGELOG.md) for more details. @@ -101,46 +99,22 @@ Create credentials and an input stream for the password vault in question: KdbxCreds creds = new KdbxCreds("123".getBytes()); InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test1.kdbx"); -then choose a database implementation, and load the database. - - Database database = SimpleDatabase.load(credentials, inputStream) - -or - - Database database = JaxbDatabase.load(credentials, inputStream) - -or - - Database database = DomDatabaseWrapper.load(credentials, inputStream) - -or +then choose the Jackson based database implementation, and load the database Database database = JacksonDatabase.load(credentials, inputStream) -Different implementations have varying characteristics, primarily speed. -The table below illustrates timings for the file `test1.kdbx` -(in the test module resources - -it is around 2k bytes and contains a few dozen entries) -as assessed by [this test](https://github.com/jorabin/KeePassJava2/blob/master/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java) - in the "examples" module. - - Simple 5 loads 20 iterations 257 millis - Jaxb 5 loads 20 iterations 326 millis - Dom 5 loads 20 iterations 758 millis - Jackson 5 loads 20 iterations 374 millis +See below for discussion of other database implementations. Note that they will not be +maintained in the future - and see the following regarding making the storage of +passwords more secure. - Simple 10 loads 1 iterations 340 millis - Jaxb 10 loads 1 iterations 552 millis - Dom 10 loads 1 iterations 175 millis - Jackson 10 loads 1 iterations 343 millis +### Storing Passwords - Simple 1 loads 50 iterations 28 millis - Jaxb 1 loads 50 iterations 47 millis - Dom 1 loads 50 iterations 251 millis - Jackson 1 loads 50 iterations 34 millis +There are numerous well-understood problems +with storing passwords as Strings in Java. See [this discussion](./PropertyValueProtection.md) about the +KeePassJava2 approach to storing passwords. -Load time is dominant in this example for JAXB and Simple, -database traversal for the DOM implementation. +> Available from release 2.2.3 the enhanced **Jackson implementation** is the only KeePassJava2 database implementation that provides a +means of storing passwords other than as String. ### Discussion @@ -173,11 +147,11 @@ It has always been the intention to support other specific password database imp Hence, the creation of abstract Database interfaces rather than following the KeePass model exactly. -KeePass is in effect defined by the code that Dominik writes to create and maintain the project. -Hence, there is not much by way of definitive specification of KeePass files other than that code. There -is a discussion of the [differences between KDBX version 3.1 and version 4](https://keepass.info/help/kb/kdbx_4.html). -There is also a discussion of the [enhancements in KDBX 4.1](https://keepass.info/help/kb/kdbx_4.1.html), as well -as a discussion of [Key Files](https://keepass.info/help/base/keys.html#keyfiles). +KeePass is in effect defined by the code that Dominik writes to create and maintain the project and +[KDBX File Format Specification](https://keepass.info/help/kb/kdbx.html) describes the file format. There +is also a discussion of the [differences between KDBX version 3.1 and version 4](https://keepass.info/help/kb/kdbx_4.html). +Additionally, there is a discussion of the [enhancements in KDBX 4.1](https://keepass.info/help/kb/kdbx_4.1.html), as well +as a discussion of [Key Files](https://keepass.info/help/base/keys.html#keyfiles). Massive credit also to the folks over at [KeePassXC](https://keepassxc.org/) who wrote some [documentation](https://github.com/keepassxreboot/keepassxc-specs) about their understanding of various format things. Also, this is a @@ -188,12 +162,34 @@ clarification and my own satisfaction I have written about my understanding of KeePass formats in the following locations: 1. The Javadoc header to [KdbxSerializer](http://javadoc.io/page/org.linguafranca.pwdb/KeePassJava2-kdbx/latest/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.html) describes KDBX stream formatting. -2. The XSD Schema [KDBX.4.xsd](KDBX.4.xsd) documents my understanding of the Keepass XML, and also my lack of understanding, in parts. +2. The XSD Schema [KDBX.4.xsd](KDBX.4.xsd) documents my understanding of the Keepass XML, and also my + lack of understanding, in parts. While preparing release 2.2.3 I found [this XSD](https://keepass.info/help/download/KDBX_XML.xsd) at the + KeePass site. I have not (so far) attempted to reconcile my documentation with it. 3. The following graphic illustrates KDBX 3.1 and 4 file formats: [![KDBX Formats](KdbxDiagram.svg "KDBX Formats")](KdbxDiagram.svg) +## Database Implementations + +KeePass - or more specifically its file format KDBX - is an XML based format, so one of the main tasks +is serializing and deserializing XML. Over time (KeePassJava2 was originally released in 2014) approaches +to Java and XML have been a bit mysterious. However, Jackson has now been chosen as the +underlying framework for implementation of KeePassJava2. + +There are several other database implementations which will be maintained for bug-fix purposes +only, with a view to being withdrawn, since they perform badly and/or depend on obsolete technology. + +- `SimpleXML` - no longer maintained, does not work with Java 17 and up +- `JAXB` - this causes problems with `javax` and `jakarta` namespaces, it's not worth maintaining + as it offers no compelling performance or other advantage +- `DOM` the was the original implementation and validates the fact that DOM based implementations + are slow. That said, if you want to load a database and then save it while maintaining whatever quirks + existed in the original database then this is the one. + +Aside from dependencies on underlying frameworks, different implementations have varying characteristics, primarily speed. This is assessed +by [this test](https://github.com/jorabin/KeePassJava2/blob/master/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java) in the module `examples`; + ## Dependencies Aside from the JRE, at release 2.2, the API depends on: @@ -202,16 +198,16 @@ Aside from the JRE, at release 2.2, the API depends on: - [Apache Commons Codec](https://commons.apache.org/proper/commons-codec/) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0)). - [Bouncy Castle](https://github.com/bcgit/bc-java/blob/master/LICENSE.html) ([MIT License](https://github.com/bcgit/bc-java/blob/master/LICENSE.html)). -The Simple XML implementation additionally depends on: - -- [Simple XML Serialisation Framework](http://simple.sourceforge.net/) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0)). -- [Faster XML Aalto](https://github.com/FasterXML/aalto-xml) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0.txt)). - The Jackson implementation depends on: - [Faster XML Jackson](https://github.com/FasterXML/jackson) -For Java 11 and later Jaxb implementation depends on explicit inclusion [no longer provided by JDK](https://docs.oracle.com/en/java/javase/11/migrate/index.html#JSMIG-GUID-F640FA9D-FB66-4D85-AD2B-D931174C09A3) of: +The (historical) Simple XML implementation additionally depends on: + +- [Simple XML Serialisation Framework](http://simple.sourceforge.net/) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0)). +- [Faster XML Aalto](https://github.com/FasterXML/aalto-xml) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0.txt)). + +For Java 11 and later, Jaxb implementation depends on explicit inclusion [no longer provided by JDK](https://docs.oracle.com/en/java/javase/11/migrate/index.html#JSMIG-GUID-F640FA9D-FB66-4D85-AD2B-D931174C09A3) of: - [JAXB](https://javaee.github.io/jaxb-v2/) @@ -304,10 +300,10 @@ In [this file](./CHANGELOG.md). ## Acknowledgements Many thanks to Pavel Ivanov [@ivanovpv](https://github.com/ivanovpv) for -his help with Android and Gradle compatibility issues. +his help with Android and Gradle compatibility issues back in the very early days. Thanks to Giuseppe Valente [@giusvale-dev](https://github.com/giusvale-dev) for -the contribution of the Jackson module and enhancements to KeyFile support. +his contribution of the Jackson module and enhancements to KeyFile support. Thanks to other contributors and raisers of issues. From e8bf9eb6777cfe17496d855eff5544974b59e9d3 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 10 Sep 2024 19:09:16 +0100 Subject: [PATCH 184/228] Typos --- PropertyValueProtection.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PropertyValueProtection.md b/PropertyValueProtection.md index 4da67d55..5233f7c4 100644 --- a/PropertyValueProtection.md +++ b/PropertyValueProtection.md @@ -19,7 +19,7 @@ What KeePass means by property protection is firstly that they are additionally overall encryption of the database file, this is so that when they are loaded as unencrypted XML they are not exposed within the process memory. Instead, they are decrypted and immediately stored in a secure way (DPAPI in Windows) so they can't be spied upon. -See [KeePass documentation in Inner Encyption](https://keepass.info/help/kb/kdbx.html#ienc ) and +See [KeePass documentation on Inner Encryption](https://keepass.info/help/kb/kdbx.html#ienc ) and [Process Memory Protection](https://keepass.info/help/base/security.html#secmemprot). In summary, they are stored in an additionally secure way (additional to file security) in XML, @@ -40,7 +40,7 @@ it runs on is more vulnerable. ### Alternatives to Storing Passwords as Strings -You are probably at least as good as Google searches as me, good chance you may be better. So you] +You are probably at least as good as Google searches as me, good chance you may be better. So you can read all about this there. #### When transferring From 45a4739d5cd502783ee5a23d2173fa62bfaad106 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 10 Sep 2024 20:18:55 +0100 Subject: [PATCH 185/228] updating tests --- .../org/linguafranca/pwdb/PropertyValue.java | 147 ++++++++++-------- .../linguafranca/pwdb/PropertyValueTest.java | 10 +- dom/pom.xml | 8 +- .../pwdb/kdbx/dom/DomPropertyValueTest.java | 57 +++++++ .../pwdb/kdbx/jackson/JacksonEntry.java | 3 - .../pwdb/JacksonPropertyValueTest.java | 17 ++ .../pwdb/kdbx/jaxb/JaxbPropertyValueTest.java | 57 +++++++ .../kdbx/simple/SimplePropertyValueTest.java | 24 ++- .../pwdb/checks/PropertyValueChecks.java | 95 ++++++++++- 9 files changed, 335 insertions(+), 83 deletions(-) create mode 100644 dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomPropertyValueTest.java create mode 100644 jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbPropertyValueTest.java diff --git a/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java b/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java index 8c078747..d4ab9ddc 100644 --- a/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java +++ b/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java @@ -19,25 +19,25 @@ * harder to access the values via a heap dump etc. */ public interface PropertyValue { - String getValueAsString(); + CharSequence getValue(); char [] getValueAsChars(); byte [] getValueAsBytes(); - CharSequence getValue(); - boolean isProtected(); + String getValueAsString(); + /** * A factory interface for PropertyValue. */ - interface Factory { - PropertyValue of (CharSequence aCharSequence); + interface Factory

    { + P of (CharSequence aCharSequence); - PropertyValue of (char [] value); + P of (char [] value); - PropertyValue of (byte [] value); + P of (byte [] value); } /** @@ -45,24 +45,28 @@ interface Factory { */ interface Strategy { /** - * A list of the properties that should be protected by default + * A list of the properties that should be protected */ List getProtectedProperties(); /** - * Whether to throw and illegalAccess Exception if an attempt is made to read a protected value as a String + * A factory for protected properties */ - boolean preventProtectedAccessAsString(); + PropertyValue.Factory newProtected(); /** - * A factory for protected properties + * A factory for unprotected property values */ - PropertyValue.Factory newProtected(); + PropertyValue.Factory newUnprotected(); /** - * A factory for unprotected property values + * Return a factory given a property name and the properties that should be protected */ - PropertyValue.Factory newUnprotected(); + default PropertyValue.Factory getFactoryFor(String propertyName) { + return getProtectedProperties().contains(propertyName) ? + newProtected() : + newUnprotected(); + } class Default implements Strategy { @@ -73,18 +77,13 @@ public List getProtectedProperties() { } @Override - public boolean preventProtectedAccessAsString() { - return false; + public Factory newProtected() { + return SealedStore.getFactory(); } @Override - public Factory newProtected() { - return new PropertyValue.SealedStore.Factory(); - } - - @Override - public Factory newUnprotected() { - return new PropertyValue.StringStore.Factory(); + public Factory newUnprotected() { + return CharsStore.getFactory(); } } } @@ -94,22 +93,26 @@ public Factory newUnprotected() { */ class StringStore implements PropertyValue { private final String value; - public static class Factory implements PropertyValue.Factory { - - @Override - public StringStore of(CharSequence aCharSequence) { - return new StringStore(aCharSequence); - } - - @Override - public StringStore of(char[] value) { - return new StringStore(value); - } - - @Override - public StringStore of(byte[] value) { - return new StringStore(value); - } + private final static PropertyValue.Factory factory = + new PropertyValue.Factory(){ + + @Override + public StringStore of(CharSequence aCharSequence) { + return new StringStore(aCharSequence); + } + + @Override + public StringStore of(char[] value) { + return new StringStore(value); + } + + @Override + public StringStore of(byte[] value) { + return new StringStore((value)); + } + }; + public static PropertyValue.Factory getFactory() { + return factory; } public StringStore(CharSequence aCharSequence){ @@ -155,24 +158,28 @@ class CharsStore implements PropertyValue, Serializable { private final char[] value; - public static class Factory implements PropertyValue.Factory { + private final static PropertyValue.Factory factory = + new PropertyValue.Factory(){ - @Override - public CharsStore of(CharSequence aCharSequence) { - return new CharsStore(aCharSequence); - } + @Override + public CharsStore of(CharSequence aCharSequence) { + return new CharsStore(aCharSequence); + } - @Override - public CharsStore of(char[] value) { - return new CharsStore(value); - } + @Override + public CharsStore of(char[] value) { + return new CharsStore(value); + } - @Override - public CharsStore of(byte[] value) { - return new CharsStore(value); - } - } + @Override + public CharsStore of(byte[] value) { + return new CharsStore((value)); + } + }; + public static PropertyValue.Factory getFactory() { + return factory; + } public CharsStore(CharSequence aString) { this.value = CharSequenceUtils.toCharArray(aString); } @@ -241,22 +248,27 @@ class SealedStore implements PropertyValue { } } - public static class Factory implements PropertyValue.Factory { + private final static PropertyValue.Factory factory = + new PropertyValue.Factory(){ - @Override - public SealedStore of(CharSequence aCharSequence) { - return new SealedStore(new CharsStore(aCharSequence)); - } + @Override + public SealedStore of(CharSequence aCharSequence) { + return new SealedStore(new CharsStore(aCharSequence)); + } - @Override - public SealedStore of(char[] value) { - return new SealedStore(new CharsStore(value)); - } + @Override + public SealedStore of(char[] value) { + return new SealedStore(new CharsStore(value)); + } - @Override - public SealedStore of(byte[] value) { - return new SealedStore(new CharsStore(value)); - } + @Override + public SealedStore of(byte[] value) { + return new SealedStore(new CharsStore(value)); + } + }; + + public static PropertyValue.Factory getFactory() { + return factory; } public SealedStore(CharsStore object){ @@ -298,7 +310,8 @@ private Key retrieveKey(ByteBuffer buffer) { public CharsStore getAsCharsStore() { try { - return ((CharsStore) sealedObject.getObject(retrieveKey(buffer))); + Key key = retrieveKey(buffer); + return ((CharsStore) sealedObject.getObject(key)); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java b/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java index 947dac0d..56a11d22 100644 --- a/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java +++ b/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java @@ -16,7 +16,7 @@ public class PropertyValueTest { @Test public void charsTest() { - PropertyValue.CharsStore.Factory factory = new PropertyValue.CharsStore.Factory(); + PropertyValue.CharsStore.Factory factory = PropertyValue.CharsStore.getFactory(); // test as CharSequence PropertyValue.CharsStore testValue = factory.of(THIS_IS_A_SECRET); @@ -40,7 +40,7 @@ public void charsTest() { @Test public void sealedObjectTest() { - PropertyValue.CharsStore.Factory factory = new PropertyValue.CharsStore.Factory(); + PropertyValue.CharsStore.Factory factory = PropertyValue.CharsStore.getFactory(); PropertyValue.CharsStore testValue = factory.of(THIS_IS_A_SECRET); PropertyValue.SealedStore sealed = new PropertyValue.SealedStore(testValue); @@ -63,9 +63,9 @@ public void sealedObjectTest() { } @Test public void stringTest() { - PropertyValue.StringStore.Factory factory = new PropertyValue.StringStore.Factory(); + PropertyValue.Factory factory = PropertyValue.StringStore.getFactory(); - PropertyValue.StringStore testValue = factory.of(THIS_IS_A_SECRET); + PropertyValue testValue = factory.of(THIS_IS_A_SECRET); assertEquals(THIS_IS_A_SECRET, testValue.getValueAsString()); testValue = factory.of(THIS_IS_A_SECRET.toCharArray()); assertEquals(THIS_IS_A_SECRET, testValue.getValueAsString()); @@ -75,7 +75,7 @@ public void stringTest() { @Test public void sealedScriptTest(){ - PropertyValue.SealedStore sealed = new PropertyValue.SealedStore.Factory().of(ANOTHER_SECRET); + PropertyValue.SealedStore sealed = PropertyValue.SealedStore.getFactory().of(ANOTHER_SECRET); PropertyValue.CharsStore charStore = sealed.getAsCharsStore(); byte[] bytes1 = charStore.getValueAsBytes(); byte[] bytes = sealed.getValueAsBytes(); diff --git a/dom/pom.xml b/dom/pom.xml index 674166f4..fa44bcb4 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -28,5 +28,11 @@ junit test - + + org.linguafranca.pwdb + test + 2.2.3-SNAPSHOT + compile + + \ No newline at end of file diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomPropertyValueTest.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomPropertyValueTest.java new file mode 100644 index 00000000..c695129a --- /dev/null +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomPropertyValueTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024 Jo Rabin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb.kdbx.dom; + +import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.checks.PropertyValueChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class DomPropertyValueTest extends PropertyValueChecks { + + public DomPropertyValueTest() throws IOException { + super(false); + } + + + @Override + public Database createDatabase() throws IOException { + return new DomDatabaseWrapper(); + } + + @Override + public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { + database.save(credentials, outputStream); + } + + @Override + public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + try { + return DomDatabaseWrapper.load(credentials, inputStream); + } catch (Exception e) { + throw new IOException(e); + } + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java index 6bd19d70..897e7952 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java @@ -141,9 +141,6 @@ public String getProperty(String s) { if (sp == null) { return null; } - if (sp.getValue().isProtected() && database.getPropertyValueStrategy().preventProtectedAccessAsString()) { - throw new IllegalStateException("Access to protected property as String is prevented by strategy"); - } return sp.getValue().getValueAsString(); } diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java index c5b8275a..b5c0b019 100644 --- a/jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java @@ -17,9 +17,13 @@ import org.linguafranca.pwdb.checks.BasicDatabaseChecks; import org.linguafranca.pwdb.checks.PropertyValueChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.KdbxHeader; import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; public class JacksonPropertyValueTest extends PropertyValueChecks { @@ -27,7 +31,20 @@ public JacksonPropertyValueTest() throws IOException { super(true); } + @Override + public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { + database.save(credentials, outputStream); + } + @Override + public JacksonDatabase loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + return JacksonDatabase.load(credentials, inputStream); + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } @Override public Database createDatabase() throws IOException { return new JacksonDatabase(); diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbPropertyValueTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbPropertyValueTest.java new file mode 100644 index 00000000..06f0d611 --- /dev/null +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbPropertyValueTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024 Jo Rabin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb.kdbx.jaxb; + +import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.checks.PropertyValueChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class JaxbPropertyValueTest extends PropertyValueChecks { + + public JaxbPropertyValueTest() throws IOException { + super(false); + } + + + @Override + public Database createDatabase() throws IOException { + return new JaxbDatabase(); + } + + @Override + public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { + database.save(credentials, outputStream); + } + + @Override + public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + try { + return JaxbDatabase.load(credentials, inputStream); + } catch (Exception e) { + throw new IOException(e); + } + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } +} diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimplePropertyValueTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimplePropertyValueTest.java index 6ab4df5a..bd81a9bc 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimplePropertyValueTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimplePropertyValueTest.java @@ -15,10 +15,14 @@ */ package org.linguafranca.pwdb.kdbx.simple; +import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.checks.PropertyValueChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; public class SimplePropertyValueTest extends PropertyValueChecks { @@ -31,5 +35,23 @@ public SimplePropertyValueTest() throws IOException { public Database createDatabase() throws IOException { return new SimpleDatabase(); } - + + @Override + public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { + database.save(credentials, outputStream); + } + + @Override + public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + try { + return SimpleDatabase.load(credentials, inputStream); + } catch (Exception e) { + throw new IOException(e); + } + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } } diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java index 19727d07..3b30f7b2 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java @@ -19,11 +19,12 @@ import org.junit.Test; import org.linguafranca.pwdb.*; -import java.io.IOException; +import java.io.*; +import java.util.ArrayList; +import java.util.List; import java.util.function.Consumer; import static org.junit.Assert.*; -import static org.junit.Assume.assumeFalse; /** * Testing the operation of PropertyValue mechanisms @@ -31,6 +32,10 @@ public abstract class PropertyValueChecks, G extends Group, E extends Entry, I extends Icon> { + public abstract void saveDatabase(D database, Credentials credentials, OutputStream outputStream) throws IOException; + public abstract D loadDatabase(Credentials credentials, InputStream inputStream) throws IOException; + public abstract Credentials getCreds(byte[] creds); + private final boolean propertyValueSupported; protected D database; @@ -47,7 +52,7 @@ public PropertyValueChecks(boolean propertyValueSupported) throws IOException { * @param f a Consumer i.e. a function that does have output * @param c the exception that is expected to be thrown when the function is executed */ - private void catchException(Consumer f, Class c) { + private void catchException(Consumer f, @SuppressWarnings("SameParameterValue") Class c) { try { f.accept(null); } catch (Exception e) { @@ -62,11 +67,11 @@ private void catchException(Consumer f, Class c) { * Check that unsupported operations throw exceptions */ @Test - public void testNoPropertyValueSupport() { - assumeFalse(propertyValueSupported); + public void noPropertyValueSupport() { + if (propertyValueSupported) return; assertFalse(database.supportsPropertyValueStrategy()); - // test a sample of the methods that are supposed to throw if used here + // test a sample of the methods that are supposed to thrown if used here catchException((z) -> database.setPropertyValueStrategy(null), UnsupportedOperationException.class); catchException((z) -> database.getPropertyValueStrategy(), UnsupportedOperationException.class); catchException((z) -> database.setShouldProtect(null, true), UnsupportedOperationException.class); @@ -76,6 +81,10 @@ public void testNoPropertyValueSupport() { catchException((z) -> entry.getPropertyValue(null), UnsupportedOperationException.class); catchException((z) -> entry.setPropertyValue(null, (PropertyValue) null), UnsupportedOperationException.class); + } + + @Test + public void checkDefaults(){ // these are the defaults established by createDatabase() assertTrue(database.shouldProtect(Entry.STANDARD_PROPERTY_NAME_PASSWORD)); assertFalse(database.shouldProtect(Entry.STANDARD_PROPERTY_NAME_USER_NAME)); @@ -84,5 +93,79 @@ public void testNoPropertyValueSupport() { assertFalse(database.shouldProtect(Entry.STANDARD_PROPERTY_NAME_TITLE)); } + @Test + public void expectedStorageType(){ + if (!propertyValueSupported) { + return; + } + + // Password is protected + PropertyValue.Strategy pvs = database.getPropertyValueStrategy(); + PropertyValue pv = pvs.getFactoryFor("Password").of("123"); + assertEquals(pv.getClass(), pvs.newProtected().of("a").getClass()); + assertTrue(pv.isProtected()); + + // Notes isn't protected + pv = pvs.getFactoryFor("Notes").of("123"); + assertEquals(pv.getClass(), pvs.newUnprotected().of("b").getClass()); + assertFalse(pv.isProtected()); + + // "random" is not protected + pv = pvs.getFactoryFor("random").of("123"); + assertEquals(pv.getClass(), pvs.newUnprotected().of("b").getClass()); + assertFalse(pv.isProtected()); + + // create a strategy that makes "random" protected + PropertyValue.Strategy newPvs = new PropertyValue.Strategy() { + @Override + public List getProtectedProperties() { + List pp = new ArrayList<>(pvs.getProtectedProperties()); + pp.add("random"); + return pp; + } + + @Override + public PropertyValue.Factory newProtected() { + return pvs.newProtected(); + } + + @Override + public PropertyValue.Factory newUnprotected() { + return pvs.newUnprotected(); + } + }; + database.setPropertyValueStrategy(newPvs); + + // get a value for random and check it is protected + pv = newPvs.getFactoryFor("random").of("123"); + assertEquals(pv.getClass(), pvs.newProtected().of("b").getClass()); + assertTrue(pv.isProtected()); + + // create an entry with random property as protected value + E entry = database.newEntry("Test Random"); + entry.setPropertyValue("random", pv); + database.getRootGroup().addEntry(entry); + + // Save database + + try { + FileOutputStream fos = new FileOutputStream("testOutput/propertyValueReload.kdbx"); + saveDatabase(database, getCreds("123".getBytes()), fos); + fos.flush(); + fos.close(); + + // reload database, "random" is still protected even though it's not protected by default + FileInputStream fis = new FileInputStream("testOutput/propertyValueReload.kdbx"); + D input = loadDatabase(getCreds("123".getBytes()), fis); + List entries = input.findEntries("random"); + assertEquals(1, entries.size()); + assertTrue(entries.get(0).getPropertyValue("random").isProtected()); + assertFalse(input.getPropertyValueStrategy().getProtectedProperties().contains("random")); + + + } catch (Exception e) { + fail(e.getMessage()); + } + } } From e3a7f66aed9349a3c14af7f90225b3117c372d16 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 10 Sep 2024 20:28:14 +0100 Subject: [PATCH 186/228] Updating documentation --- CHANGELOG.md | 12 ++++++++++++ readme.md | 5 ++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92ddc6c3..c7a0aead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.com) and [Semantic Versioning](http://semver.org/spec/v2.0.0.html) +## [2.2.3-SNAPSHOT] + +### Added + +- implementation of property value storage interface PropertyValue to allow memory protection of sensitive values +- Default implementations of protected and unprotected storage + +### Changed + +- Jackson implementation supports this interface +- Other implementations throw exceptions appropriately as unsupported + ## [2.2.2] 2024-09-06 ### Added diff --git a/readme.md b/readme.md index e43e51dd..75c72284 100644 --- a/readme.md +++ b/readme.md @@ -285,7 +285,10 @@ load and save stuff that the implementation doesn't specifically know about. But Then came the JAXB implementation, but belatedly it seems that Android support is in question. So latterly the Simple implementation. That was probably enough -KDBX implementations, however, the Simple XML library seems no longer to be maintained, and along comes the Jackson Implementation. +KDBX implementations, however, the Simple XML library seems no longer to be maintained, +and along comes the Jackson Implementation. + +Jackson is the implementation going forward. ### Gradle From e87a97d47a56b39eab2035136e5f53fa007bcf1d Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 10 Sep 2024 20:56:03 +0100 Subject: [PATCH 187/228] Attempt to remedy CircleCI file access problem --- .../java/org/linguafranca/pwdb/checks/PropertyValueChecks.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java index 3b30f7b2..54a6b5dd 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java @@ -154,6 +154,9 @@ public PropertyValue.Factory newUnprotected() { fos.flush(); fos.close(); + // wait for file to save + Thread.sleep(1000); + // reload database, "random" is still protected even though it's not protected by default FileInputStream fis = new FileInputStream("testOutput/propertyValueReload.kdbx"); D input = loadDatabase(getCreds("123".getBytes()), fis); From 7d9938e72cbddbcb88e9a7263e90bf6740c2d2ee Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 10 Sep 2024 21:42:03 +0100 Subject: [PATCH 188/228] try again for Circle CI --- .../pwdb/checks/PropertyValueChecks.java | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java index 54a6b5dd..c1bca9bc 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java @@ -94,7 +94,7 @@ public void checkDefaults(){ } @Test - public void expectedStorageType(){ + public void expectedStorageType() throws IOException { if (!propertyValueSupported) { return; } @@ -148,27 +148,19 @@ public PropertyValue.Factory newUnprotected() { // Save database - try { - FileOutputStream fos = new FileOutputStream("testOutput/propertyValueReload.kdbx"); - saveDatabase(database, getCreds("123".getBytes()), fos); - fos.flush(); - fos.close(); - - // wait for file to save - Thread.sleep(1000); + FileOutputStream fos = new FileOutputStream("testOutput/propertyValueReload.kdbx"); + saveDatabase(database, getCreds("123".getBytes()), fos); + fos.flush(); + fos.close(); - // reload database, "random" is still protected even though it's not protected by default - FileInputStream fis = new FileInputStream("testOutput/propertyValueReload.kdbx"); - D input = loadDatabase(getCreds("123".getBytes()), fis); - List entries = input.findEntries("random"); - assertEquals(1, entries.size()); - assertTrue(entries.get(0).getPropertyValue("random").isProtected()); - assertFalse(input.getPropertyValueStrategy().getProtectedProperties().contains("random")); + // reload database, "random" is still protected even though it's not protected by default + FileInputStream fis = new FileInputStream("testOutput/propertyValueReload.kdbx"); + D input = loadDatabase(getCreds("123".getBytes()), fis); - - } catch (Exception e) { - fail(e.getMessage()); - } - } + List entries = input.findEntries("random"); + assertEquals(1, entries.size()); + assertTrue(entries.get(0).getPropertyValue("random").isProtected()); + assertFalse(input.getPropertyValueStrategy().getProtectedProperties().contains("random")); +} } From 117dee4187ac6327b410699876888166de13608b Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 10 Sep 2024 21:54:13 +0100 Subject: [PATCH 189/228] try new filename --- .../org/linguafranca/pwdb/checks/PropertyValueChecks.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java index c1bca9bc..084f43dd 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java @@ -148,13 +148,13 @@ public PropertyValue.Factory newUnprotected() { // Save database - FileOutputStream fos = new FileOutputStream("testOutput/propertyValueReload.kdbx"); + FileOutputStream fos = new FileOutputStream("testOutput/test9.kdbx"); saveDatabase(database, getCreds("123".getBytes()), fos); fos.flush(); fos.close(); // reload database, "random" is still protected even though it's not protected by default - FileInputStream fis = new FileInputStream("testOutput/propertyValueReload.kdbx"); + FileInputStream fis = new FileInputStream("testOutput/test9.kdbx"); D input = loadDatabase(getCreds("123".getBytes()), fis); List entries = input.findEntries("random"); From aaa67cca1a80e132b7eb7730938bab6567f60ab5 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 10 Sep 2024 21:59:52 +0100 Subject: [PATCH 190/228] remove file access --- .../org/linguafranca/pwdb/checks/PropertyValueChecks.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java index 084f43dd..ddb98dce 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java @@ -141,7 +141,7 @@ public PropertyValue.Factory newUnprotected() { assertEquals(pv.getClass(), pvs.newProtected().of("b").getClass()); assertTrue(pv.isProtected()); - // create an entry with random property as protected value + /*// create an entry with random property as protected value E entry = database.newEntry("Test Random"); entry.setPropertyValue("random", pv); database.getRootGroup().addEntry(entry); @@ -160,7 +160,7 @@ public PropertyValue.Factory newUnprotected() { List entries = input.findEntries("random"); assertEquals(1, entries.size()); assertTrue(entries.get(0).getPropertyValue("random").isProtected()); - assertFalse(input.getPropertyValueStrategy().getProtectedProperties().contains("random")); + assertFalse(input.getPropertyValueStrategy().getProtectedProperties().contains("random"));*/ } } From 878f417989052722390a9a48eeec5bf6af48e850 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 10 Sep 2024 22:11:35 +0100 Subject: [PATCH 191/228] try again Circle CI --- .../org/linguafranca/pwdb/JacksonPropertyValueTest.java | 9 ++++++--- .../linguafranca/pwdb/checks/PropertyValueChecks.java | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java index b5c0b019..40571e53 100644 --- a/jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java @@ -20,19 +20,22 @@ import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.kdbx.KdbxHeader; import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; +import org.linguafranca.pwdb.kdbx.jackson.JacksonEntry; +import org.linguafranca.pwdb.kdbx.jackson.JacksonGroup; +import org.linguafranca.pwdb.kdbx.jackson.JacksonIcon; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -public class JacksonPropertyValueTest extends PropertyValueChecks { +public class JacksonPropertyValueTest extends PropertyValueChecks { public JacksonPropertyValueTest() throws IOException { super(true); } @Override - public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { + public void saveDatabase(JacksonDatabase database, Credentials credentials, OutputStream outputStream) throws IOException { database.save(credentials, outputStream); } @@ -46,7 +49,7 @@ public Credentials getCreds(byte[] creds) { return new KdbxCreds(creds); } @Override - public Database createDatabase() throws IOException { + public JacksonDatabase createDatabase() throws IOException { return new JacksonDatabase(); } diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java index ddb98dce..084f43dd 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java @@ -141,7 +141,7 @@ public PropertyValue.Factory newUnprotected() { assertEquals(pv.getClass(), pvs.newProtected().of("b").getClass()); assertTrue(pv.isProtected()); - /*// create an entry with random property as protected value + // create an entry with random property as protected value E entry = database.newEntry("Test Random"); entry.setPropertyValue("random", pv); database.getRootGroup().addEntry(entry); @@ -160,7 +160,7 @@ public PropertyValue.Factory newUnprotected() { List entries = input.findEntries("random"); assertEquals(1, entries.size()); assertTrue(entries.get(0).getPropertyValue("random").isProtected()); - assertFalse(input.getPropertyValueStrategy().getProtectedProperties().contains("random"));*/ + assertFalse(input.getPropertyValueStrategy().getProtectedProperties().contains("random")); } } From e42e536ab03409c339c4d69e7a952dcf4a956f05 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 10 Sep 2024 22:19:18 +0100 Subject: [PATCH 192/228] it's going to work this time --- .../org/linguafranca/pwdb/checks/PropertyValueChecks.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java index 084f43dd..ec9a153a 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java @@ -16,10 +16,13 @@ package org.linguafranca.pwdb.checks; +import org.junit.BeforeClass; import org.junit.Test; import org.linguafranca.pwdb.*; import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @@ -39,6 +42,11 @@ public abstract class PropertyValueChecks, G exte private final boolean propertyValueSupported; protected D database; + @BeforeClass + public static void ensureOutputDir() throws IOException { + Files.createDirectories(Paths.get("testOutput")); + } + public PropertyValueChecks(boolean propertyValueSupported) throws IOException { this.database = createDatabase(); this.propertyValueSupported = propertyValueSupported; From cc604c5f3ed19b1d3d66dbb6b21fa8adb527cee3 Mon Sep 17 00:00:00 2001 From: Giuseppe Valente Date: Thu, 12 Sep 2024 00:27:06 +0200 Subject: [PATCH 193/228] Explained the security technical choices in the README. --- PropertyValueProtection.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/PropertyValueProtection.md b/PropertyValueProtection.md index 5233f7c4..97014b70 100644 --- a/PropertyValueProtection.md +++ b/PropertyValueProtection.md @@ -58,15 +58,19 @@ luck trying to avoid strings at all. Likewise, if you collect passwords from a d #### At rest -So, as to how to store protected data "at rest" in the application? As noted, using anything other -than a String improves the situation. Obfuscation of the data using a hash may improve it -further, but a determined attacker will know (or might be able to find) the hash key anyway. - -Storing the data in an encrypted form could be attractive. Leaving it in the encrypted form that -it appears in the KDBX InnerStream is not practical, as this stream encrypted approach -depends on the encrypted property values appearing in the same order for encryption and decryption, and -it would not be practical to encrypt/decrypt all protected fields when accessing or manipulating -any one of them. +After the deserialization process, the passwords are stored in RAM. Therefore: + +- **Hashing the password is not feasible**, as the hash function is a one-way process. +This means that once a password is hashed, it cannot be reverted back to its original form, +which makes hashing unsuitable in this context. Since we need to retrieve and view +the saved passwords (as per the requirements), hashing does not meet the objective. + +- **Encrypting and decrypting data in RAM is also problematic.** The question arises: +What key should be used for this process? If the key is stored in RAM, we are essentially creating +the same security vulnerability we are trying to avoid. +While storing data in an encrypted form may seem appealing, keeping it encrypted in the KDBX InnerStream is impractical. +This approach requires that the encrypted property values appear in the same order for both encryption and decryption, which makes it cumbersome. +Encrypting and decrypting all protected fields each time we need to access or manipulate just one of them would add significant overhead. ## KeePassJava2 2.2.3 Property Value Strategy From f72481e7d8095c0ba5289d706f404333bab6ff51 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Thu, 12 Sep 2024 12:52:04 +0100 Subject: [PATCH 194/228] Significant updates to PropertyyValue --- .../org/linguafranca/pwdb/PropertyValue.java | 235 ++++++++++++------ .../linguafranca/pwdb/PropertyValueTest.java | 90 ++++--- 2 files changed, 215 insertions(+), 110 deletions(-) diff --git a/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java b/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java index d4ab9ddc..07fe661c 100644 --- a/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java +++ b/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java @@ -1,14 +1,21 @@ package org.linguafranca.pwdb; -import org.apache.commons.lang3.CharSequenceUtils; - -import javax.crypto.*; - -import java.io.*; +import com.google.common.base.Charsets; + +import javax.crypto.Cipher; +import javax.crypto.SealedObject; +import javax.crypto.SecretKey; +import javax.security.auth.DestroyFailedException; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; -import java.security.Key; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -29,6 +36,35 @@ public interface PropertyValue { String getValueAsString(); + static byte[] charsToBytes(char[] value) { + CharBuffer cb = CharBuffer.wrap(value); + ByteBuffer bb = StandardCharsets.UTF_8.encode(cb); + byte[] result = new byte[bb.limit()]; + bb.get(result); + return result; + } + + static char[] bytesToChars(byte[] value) { + ByteBuffer bb = ByteBuffer.wrap(value); + CharBuffer cb = Charsets.UTF_8.decode(bb); + char[] chars = new char[cb.limit()]; + cb.get(chars); + return chars; + } + + static byte[] charSequenceToBytes(CharSequence charSequence){ + CharBuffer cb = CharBuffer.wrap(charSequence); + ByteBuffer bb = StandardCharsets.UTF_8.encode(cb); + byte[] result = new byte[bb.limit()]; + bb.get(result); + return result; + } + + static CharSequence bytesToCharSequence(byte[] bytes){ + ByteBuffer bb = ByteBuffer.wrap(bytes); + return StandardCharsets.UTF_8.decode(bb); + } + /** * A factory interface for PropertyValue. */ @@ -83,7 +119,7 @@ public Factory newProtected() { @Override public Factory newUnprotected() { - return CharsStore.getFactory(); + return BytesStore.getFactory(); } } } @@ -152,72 +188,65 @@ public boolean isProtected() { } /** - * Property values are stored as char arrays. + * Property values are stored as byte arrays. */ - class CharsStore implements PropertyValue, Serializable { + class BytesStore implements PropertyValue, Serializable { - private final char[] value; + private final byte[] value; - private final static PropertyValue.Factory factory = - new PropertyValue.Factory(){ + private final static PropertyValue.Factory factory = + new PropertyValue.Factory(){ @Override - public CharsStore of(CharSequence aCharSequence) { - return new CharsStore(aCharSequence); + public BytesStore of(CharSequence aCharSequence) { + return new BytesStore(aCharSequence); } @Override - public CharsStore of(char[] value) { - return new CharsStore(value); + public BytesStore of(char[] value) { + return new BytesStore(value); } @Override - public CharsStore of(byte[] value) { - return new CharsStore((value)); + public BytesStore of(byte[] value) { + return new BytesStore((value)); } }; - public static PropertyValue.Factory getFactory() { + public static PropertyValue.Factory getFactory() { return factory; } - public CharsStore(CharSequence aString) { - this.value = CharSequenceUtils.toCharArray(aString); + public BytesStore(CharSequence aString) { + this.value = charSequenceToBytes(aString); } - public CharsStore(char [] value) { - this.value = value; + public BytesStore(char [] value) { + this.value = charsToBytes(value); } - public CharsStore(byte [] value) { - ByteBuffer bb = ByteBuffer.wrap(value); - CharBuffer cb = StandardCharsets.UTF_8.decode(bb); - char[] chars = new char[cb.limit()]; - cb.get(chars); - this.value = chars; + public BytesStore(byte [] value) { + this.value = Arrays.copyOf(value, value.length); } @Override public String getValueAsString() { - return CharBuffer.wrap(this.value).toString(); + return new String(this.value); } @Override public CharSequence getValue() { - return CharBuffer.wrap(this.value); + ByteBuffer bb = ByteBuffer.wrap(this.value); + return Charsets.UTF_8.decode(bb); } @Override public char [] getValueAsChars() { - return this.value; + return bytesToChars(this.value); } @Override public byte [] getValueAsBytes() { - CharBuffer cb = CharBuffer.wrap(this.value); - ByteBuffer bb = StandardCharsets.UTF_8.encode(cb); - byte[] result = new byte[bb.limit()]; - bb.get(result); - return result; + return Arrays.copyOf(this.value, this.value.length); } @Override @@ -236,13 +265,40 @@ public boolean isProtected() { * for storage off-heap may be significant. */ class SealedStore implements PropertyValue { - static KeyGenerator keyGenerator; + static SecureRandom secureRandom = new SecureRandom(); private final SealedObject sealedObject; private final ByteBuffer buffer; - static { + private ByteBuffer storeKey(SecretKey key) throws IOException { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(key); + ByteBuffer b = ByteBuffer.allocateDirect(baos.size()); + b.put(baos.toByteArray()); + return b; + } + } + + private SecretKey retrieveKey(ByteBuffer buffer) { + byte [] bytes = new byte[buffer.position()]; + buffer.rewind(); + buffer.get(bytes); + try { + try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais)) { + return (SecretKey) ois.readObject(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private byte[] getBytes() { try { - keyGenerator= KeyGenerator.getInstance("AES"); + SecretKey key = retrieveKey(buffer); + byte[] cs = ((byte[]) sealedObject.getObject(key)); + key.destroy(); + return cs; } catch (Exception e) { throw new RuntimeException(e); } @@ -253,17 +309,17 @@ class SealedStore implements PropertyValue { @Override public SealedStore of(CharSequence aCharSequence) { - return new SealedStore(new CharsStore(aCharSequence)); + return new SealedStore(aCharSequence); } @Override public SealedStore of(char[] value) { - return new SealedStore(new CharsStore(value)); + return new SealedStore(value); } @Override public SealedStore of(byte[] value) { - return new SealedStore(new CharsStore(value)); + return new SealedStore(value); } }; @@ -271,69 +327,86 @@ public static PropertyValue.Factory getFactory() { return factory; } - public SealedStore(CharsStore object){ - try { - Key key = keyGenerator.generateKey(); - buffer = storeKey(key); - Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - cipher.init(Cipher.ENCRYPT_MODE, key); - sealedObject = new SealedObject(object, cipher); - } catch (Exception e) { - throw new RuntimeException(e); + /** + * Believe it or not ... a SecretKey generated by the standard KeyGenerator throws an + * exception when the destroy() method is called, so we roll our own + */ + private static class AESKey implements SecretKey, Serializable { + private boolean destroyed = false; + private final byte[] key = new byte[16]; + final String algorithm = "AES"; + + { + secureRandom.nextBytes(key); } - } - private ByteBuffer storeKey(Key key) throws IOException { - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(baos)) { - oos.writeObject(key); - ByteBuffer b = ByteBuffer.allocateDirect(baos.size()); - b.put(baos.toByteArray()); - return b; + @Override + public void destroy() { + Arrays.fill(this.key, (byte) 0); + this.destroyed = true; } - } - private Key retrieveKey(ByteBuffer buffer) { - byte [] bytes = new byte[buffer.position()]; - buffer.rewind(); - buffer.get(bytes); - try { - try ( - ByteArrayInputStream bais = new ByteArrayInputStream(bytes); - ObjectInputStream ois = new ObjectInputStream(bais)) { - return (Key) ois.readObject(); - } - } catch (Exception e) { - throw new RuntimeException(e); + @Override + public boolean isDestroyed() { + return this.destroyed; + } + + @Override + public String getAlgorithm() { + return this.algorithm; + } + + @Override + public String getFormat() { + return "RAW"; + } + + @Override + public byte[] getEncoded() { + return this.key.clone(); } } - public CharsStore getAsCharsStore() { + public SealedStore(byte [] bytes){ try { - Key key = retrieveKey(buffer); - return ((CharsStore) sealedObject.getObject(key)); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + SecretKey key = new AESKey(); + cipher.init(Cipher.ENCRYPT_MODE, key); + buffer = storeKey(key); + key.destroy(); + sealedObject = new SealedObject(bytes, cipher); } catch (Exception e) { throw new RuntimeException(e); } } - @Override + + public SealedStore(char [] chars) { + this(charsToBytes(chars)); + } + + public SealedStore(CharSequence charSequence) { + this(charSequenceToBytes(charSequence)); + } + + @Override public String getValueAsString() { - return getAsCharsStore().getValueAsString(); + return new String(getBytes()); } @Override public char[] getValueAsChars() { - return getAsCharsStore().getValueAsChars(); + return bytesToChars(getBytes()); } @Override public byte[] getValueAsBytes() { - return getAsCharsStore().getValueAsBytes(); + byte [] result = getBytes(); + return Arrays.copyOf(result, result.length); } @Override public CharSequence getValue() { - return getAsCharsStore().getValue(); + return bytesToCharSequence(getBytes()); } @Override diff --git a/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java b/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java index 56a11d22..21918401 100644 --- a/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java +++ b/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java @@ -15,11 +15,11 @@ public class PropertyValueTest { Logger logger = LoggerFactory.getLogger(PropertyValueTest.class); @Test - public void charsTest() { - PropertyValue.CharsStore.Factory factory = PropertyValue.CharsStore.getFactory(); + public void bytesTest() { + PropertyValue.BytesStore.Factory factory = PropertyValue.BytesStore.getFactory(); // test as CharSequence - PropertyValue.CharsStore testValue = factory.of(THIS_IS_A_SECRET); + PropertyValue.BytesStore testValue = factory.of(THIS_IS_A_SECRET); assertEquals(THIS_IS_A_SECRET, testValue.getValue().toString()); assertEquals(THIS_IS_A_SECRET, testValue.getValueAsString()); assertArrayEquals(THIS_IS_A_SECRET.toCharArray(), testValue.getValueAsChars()); @@ -40,47 +40,79 @@ public void charsTest() { @Test public void sealedObjectTest() { - PropertyValue.CharsStore.Factory factory = PropertyValue.CharsStore.getFactory(); + PropertyValue.SealedStore.Factory factory = PropertyValue.SealedStore.getFactory(); - PropertyValue.CharsStore testValue = factory.of(THIS_IS_A_SECRET); - PropertyValue.SealedStore sealed = new PropertyValue.SealedStore(testValue); - assertEquals(testValue.getValue(), sealed.getValue()); - assertArrayEquals(testValue.getValueAsBytes(), sealed.getValueAsBytes()); - assertArrayEquals(testValue.getValueAsChars(), sealed.getValueAsChars()); - assertEquals(testValue.getValueAsString(), sealed.getValueAsString()); + PropertyValue.SealedStore sealed = factory.of(THIS_IS_A_SECRET); + assertEquals(THIS_IS_A_SECRET, sealed.getValue().toString()); + assertArrayEquals(THIS_IS_A_SECRET.getBytes(), sealed.getValueAsBytes()); + assertArrayEquals(THIS_IS_A_SECRET.toCharArray(), sealed.getValueAsChars()); + assertEquals(THIS_IS_A_SECRET, sealed.getValueAsString()); - testValue = factory.of(THIS_IS_A_SECRET.getBytes()); - sealed = new PropertyValue.SealedStore(testValue); - assertArrayEquals(testValue.getValueAsBytes(), sealed.getValueAsBytes()); - assertArrayEquals(testValue.getValueAsChars(), sealed.getValueAsChars()); - assertEquals(testValue.getValueAsString(), sealed.getValueAsString()); + sealed = factory.of(THIS_IS_A_SECRET.getBytes()); + assertEquals(THIS_IS_A_SECRET, sealed.getValue().toString()); + assertArrayEquals(THIS_IS_A_SECRET.getBytes(), sealed.getValueAsBytes()); + assertArrayEquals(THIS_IS_A_SECRET.toCharArray(), sealed.getValueAsChars()); + assertEquals(THIS_IS_A_SECRET, sealed.getValueAsString()); - testValue = factory.of(THIS_IS_A_SECRET.toCharArray()); - sealed = new PropertyValue.SealedStore(testValue); - assertArrayEquals(testValue.getValueAsBytes(), sealed.getValueAsBytes()); - assertArrayEquals(testValue.getValueAsChars(), sealed.getValueAsChars()); - assertEquals(testValue.getValueAsString(), sealed.getValueAsString()); + sealed =factory.of(THIS_IS_A_SECRET.toCharArray()); + assertEquals(THIS_IS_A_SECRET, sealed.getValue().toString()); + assertArrayEquals(THIS_IS_A_SECRET.getBytes(), sealed.getValueAsBytes()); + assertArrayEquals(THIS_IS_A_SECRET.toCharArray(), sealed.getValueAsChars()); + assertEquals(THIS_IS_A_SECRET, sealed.getValueAsString()); } @Test public void stringTest() { PropertyValue.Factory factory = PropertyValue.StringStore.getFactory(); - PropertyValue testValue = factory.of(THIS_IS_A_SECRET); - assertEquals(THIS_IS_A_SECRET, testValue.getValueAsString()); - testValue = factory.of(THIS_IS_A_SECRET.toCharArray()); - assertEquals(THIS_IS_A_SECRET, testValue.getValueAsString()); - testValue = factory.of(THIS_IS_A_SECRET.getBytes(StandardCharsets.UTF_8)); - assertEquals(THIS_IS_A_SECRET, testValue.getValueAsString()); + PropertyValue.StringStore stringStore = factory.of(THIS_IS_A_SECRET); + assertEquals(THIS_IS_A_SECRET, stringStore.getValue()); + assertArrayEquals(THIS_IS_A_SECRET.getBytes(), stringStore.getValueAsBytes()); + assertArrayEquals(THIS_IS_A_SECRET.toCharArray(), stringStore.getValueAsChars()); + assertEquals(THIS_IS_A_SECRET, stringStore.getValueAsString()); + + stringStore = factory.of(THIS_IS_A_SECRET.getBytes()); + assertEquals(THIS_IS_A_SECRET, stringStore.getValue()); + assertArrayEquals(THIS_IS_A_SECRET.getBytes(), stringStore.getValueAsBytes()); + assertArrayEquals(THIS_IS_A_SECRET.toCharArray(), stringStore.getValueAsChars()); + assertEquals(THIS_IS_A_SECRET, stringStore.getValueAsString()); + + stringStore =factory.of(THIS_IS_A_SECRET.toCharArray()); + assertEquals(THIS_IS_A_SECRET, stringStore.getValue()); + assertArrayEquals(THIS_IS_A_SECRET.getBytes(), stringStore.getValueAsBytes()); + assertArrayEquals(THIS_IS_A_SECRET.toCharArray(), stringStore.getValueAsChars()); + assertEquals(THIS_IS_A_SECRET, stringStore.getValueAsString()); } @Test public void sealedScriptTest(){ PropertyValue.SealedStore sealed = PropertyValue.SealedStore.getFactory().of(ANOTHER_SECRET); - PropertyValue.CharsStore charStore = sealed.getAsCharsStore(); - byte[] bytes1 = charStore.getValueAsBytes(); byte[] bytes = sealed.getValueAsBytes(); - byte[] answer = ANOTHER_SECRET.getBytes(StandardCharsets.UTF_8); assertArrayEquals(ANOTHER_SECRET.getBytes(StandardCharsets.UTF_8), bytes); } + + @Test + public void getCharsTest(){ + PropertyValue.BytesStore cs = new PropertyValue.BytesStore("a test".toCharArray()); + char [] value = cs.getValueAsChars(); + value[0] = 'b'; + // changing the retrieved copy doesn't change the source + assertArrayEquals("a test".toCharArray(), cs.getValueAsChars()); + + byte [] bytes = cs.getValueAsBytes(); + bytes [0] = 0; + // same for bytes + assertArrayEquals("a test".getBytes(), cs.getValueAsBytes()); + } + + @Test + public void putCharsTest(){ + String testValue = "a test"; + char [] chars = testValue.toCharArray(); + PropertyValue.BytesStore cs = new PropertyValue.BytesStore(chars); + chars[0] = 'b'; + char [] value = cs.getValueAsChars(); + // changing the source doesn't change the copy + assertArrayEquals(testValue.toCharArray(), cs.getValueAsChars()); + } } \ No newline at end of file From a6e64f3302ab96d070af9fb31567d9db28600c06 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Thu, 12 Sep 2024 13:22:43 +0100 Subject: [PATCH 195/228] Clarifications of the discussion of property value protection --- PropertyValueProtection.md | 45 +++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/PropertyValueProtection.md b/PropertyValueProtection.md index 97014b70..3c17b78f 100644 --- a/PropertyValueProtection.md +++ b/PropertyValueProtection.md @@ -6,7 +6,7 @@ KeePass allows distinguishing more sensitive string valued properties from less that have sensitive values are called "Protected Properties". KDBX files contain a list of the standard properties and whether they are to be treated as protected -by default. The standard properties include password, which is so treated. There doesn't seem to be a way of updating +by default. The standard properties include Password, which is so treated. There doesn't seem to be a way of updating this list in the Windows KeePass implementation, and in any case documentation says that the list is updated to default after load of a database, so it seems that it ignored. @@ -31,12 +31,14 @@ and they are stored in a secure way in memory. All property values are stored as Strings in process memory, meaning that they can be seen via process dumps and also, worse, Java Strings are immutable and this means that passwords stored as Strings -end up in the String pool, can't be zeroed and will be garbage collected only when they are. +end up in the String pool, can't be zeroed and will be removed only when garbage collected. I don't regard this is a *terrible* problem, but you don't have to try hard find loads of discussion about how *awful* it is on Google, and for sure, it's certainly not as good as it could be for more security conscious applications, or applications where the infrastructure -it runs on is more vulnerable. +it runs on is more vulnerable. I thought [this answer](https://stackoverflow.com/a/66287347) on StackOverflow +put it all in perspective quite well (the original question being why trying to destroy a SecretKey +raises an exception, which does seem extremely odd). ### Alternatives to Storing Passwords as Strings @@ -60,26 +62,37 @@ luck trying to avoid strings at all. Likewise, if you collect passwords from a d After the deserialization process, the passwords are stored in RAM. Therefore: -- **Hashing the password is not feasible**, as the hash function is a one-way process. +**Hashing the password is not feasible**, as the hash function is a one-way process. This means that once a password is hashed, it cannot be reverted back to its original form, which makes hashing unsuitable in this context. Since we need to retrieve and view the saved passwords (as per the requirements), hashing does not meet the objective. -- **Encrypting and decrypting data in RAM is also problematic.** The question arises: +**Encrypting and decrypting data in RAM is also problematic.** The question arises: What key should be used for this process? If the key is stored in RAM, we are essentially creating the same security vulnerability we are trying to avoid. + While storing data in an encrypted form may seem appealing, keeping it encrypted in the KDBX InnerStream is impractical. This approach requires that the encrypted property values appear in the same order for both encryption and decryption, which makes it cumbersome. Encrypting and decrypting all protected fields each time we need to access or manipulate just one of them would add significant overhead. +If the data is to be encrypted, then we need to find some way of storing the encryption key +that is not open to simple inspection. + ## KeePassJava2 2.2.3 Property Value Strategy In the end it's up to the user of the library to decide what is the right approach to the trade-off between vulnerability, risk and increased resource consumption. +Because of this, KeePassJava2 provides an interface for the storage and retrieval of property values +and while it provides some default implementations, users should form +their own opinions as to their suitability for their use cases. + ### Retrieval and Storage of Property Values -Because of this, KeePassJava2 provides an interface for the storage and retrieval of property values. +Since the protected value has to be available in an unprotected form for storage and for use, it's +up to the user to minimise the length of time that this is the case and to make sure that where +possible the data structures used are cleared after use (and as noted, in particular to avoid the use of String). + ```Java public interface PropertyValue { CharSequence getValue(); @@ -124,7 +137,9 @@ any implementation or strategy to store a value. On save of the database, property values are saved to the inner stream as protected if the `isProtected()` method of their implementation returns `true`. On reload, they will be stored -using the strategy defined default class for whether they are protected or not. When a database is +using the class defined by the Strategy appropriate to whether they are protected or not. + +When a database is reloaded the default protected properties when it was saved will not be reloaded, as there's nowhere in KDBX to store this information in a standardized way. (True, it could be stored in a non-standard way.) @@ -157,23 +172,27 @@ interface Strategy { } } ``` + #### Default Implementations of PropertyValue There are three default implementations of `PropertyValue`: - `PropertyValue.StringStore` stores values as strings -- `PropertyValue.CharsStore` stores values as char[] -- `PropertyValue.SealedStore` stores values as `javax.crypto.SealedObject` and stores the key using a `ByteBuffer` -obtained using the `ByteBuffer.allocateDirect()` method. +- `PropertyValue.BytesStore` stores values as byte[] +- `PropertyValue.SealedStore` stores values as `javax.crypto.SealedObject` +and stores the key using a `ByteBuffer` obtained using the `ByteBuffer.allocateDirect()` method +in order to try to have the key stored off-heap + #### Default Implementation of Strategy -- `PropertyValue.Strategy.Default` defines `passowrd` as the only protected value and `CharsStore` and +`PropertyValue.Strategy.Default` defines `Password` as the only protected value and `BytesStore` and `SealedStore` as the unprotected and protected `PropertyValue` implementations. ### Implementation in Databases From KeepassJava2 2.2.3 the Jackson implementation supports setting and getting of `PropertyValue`s from an `Entry`. -It supports setting and getting of `Strategy` from `Database`. +It supports setting and getting of `Strategy` from `JacksonDatabase`. -If `Database.supportsPropertyValueStrategy()` returns false, then attempts to use any methods associated with PropertyValue from other database implementations +For other database implementations `Database.supportsPropertyValueStrategy()` returns false, +and attempts to use any methods associated with PropertyValue cause an `UnsupportedOperationException` to be raised. From b166c4590b29d4ebbd6845f985a42cbb477896f9 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Thu, 12 Sep 2024 16:19:43 +0100 Subject: [PATCH 196/228] Some tweaks --- PropertyValueProtection.md | 11 +++++++++-- database/pom.xml | 5 ----- .../pwdb/kdbx/jackson/JacksonDatabase.java | 5 +++-- .../kdbx/jackson/JacksonSerializableDatabase.java | 15 ++++++++++++--- .../kdbx/jackson/converter/ValueSerializer.java | 4 +++- readme.md | 2 +- 6 files changed, 28 insertions(+), 14 deletions(-) diff --git a/PropertyValueProtection.md b/PropertyValueProtection.md index 3c17b78f..ccb6f87d 100644 --- a/PropertyValueProtection.md +++ b/PropertyValueProtection.md @@ -140,11 +140,18 @@ On save of the database, property values are saved to the inner stream as protec using the class defined by the Strategy appropriate to whether they are protected or not. When a database is -reloaded the default protected properties when it was saved will not be reloaded, as there's nowhere -in KDBX to store this information in a standardized way. (True, it could be stored in a non-standard way.) +reloaded the default protected properties when it was saved will not be reloaded. Other than for the default properties +of `Title`, `URL`, `UserName`, `Title` and `Notes` this information can't be saved in a standardized +way in the KDBX format, and as noted below this information is ignored by the Windows KeePass implementation. Changing the strategy doesn't alter the way that existing values are stored in the database. +**Note** KeePass (Windows) does the following: on load it loads values as protected if they +are marked as such in the database file. It resets the default protection from however it +is defined in the file to the default, +which is to protect the `Password` property and nothing else. When saving it saves properties +that are protected in memory as protected, but additionally protects `Password` property values. + ```Java interface Strategy { /** diff --git a/database/pom.xml b/database/pom.xml index c4cf081f..f4095dec 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -27,10 +27,5 @@ org.bouncycastle bcpkix-jdk18on - - org.apache.commons - commons-lang3 - 3.15.0 - \ No newline at end of file diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java index 84ae3a13..d17615d5 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java @@ -108,8 +108,9 @@ public void save(Credentials credentials, OutputStream outputStream) throws IOEx public void save(StreamFormat streamFormat, Credentials credentials, OutputStream outputStream) throws IOException { keePassFile.meta.generator = "KeePassJava2-Jackson"; - JacksonSerializableDatabase simpleSerializableDatabase = new JacksonSerializableDatabase(this.keePassFile); - streamFormat.save(simpleSerializableDatabase, credentials, outputStream); + JacksonSerializableDatabase jacksonSerializableDatabase = new JacksonSerializableDatabase(this.keePassFile); + jacksonSerializableDatabase.setPropertyValueStrategy(this.getPropertyValueStrategy()); + streamFormat.save(jacksonSerializableDatabase, credentials, outputStream); setDirty(false); } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java index 7cb11432..a3f9d10a 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java @@ -43,6 +43,8 @@ public class JacksonSerializableDatabase implements SerializableDatabase { public KeePassFile keePassFile; private StreamEncryptor encryptor; + private PropertyValue.Strategy propertyValueStrategy = new PropertyValue.Strategy.Default(); + public static KeePassFile createEmptyDatabase() throws IOException { InputStream inputStream = JacksonSerializableDatabase.class.getClassLoader() @@ -63,7 +65,7 @@ public JacksonSerializableDatabase(KeePassFile keePassFile) { public JacksonSerializableDatabase load(InputStream inputStream) throws IOException { XmlMapper mapper = new XmlMapper(); SimpleModule module = new SimpleModule(); - module.addDeserializer(PropertyValue.class, new ValueDeserializer(encryptor, new PropertyValue.Strategy.Default())); + module.addDeserializer(PropertyValue.class, new ValueDeserializer(encryptor, propertyValueStrategy)); mapper.registerModule(module); keePassFile = mapper.readValue(inputStream, KeePassFile.class); return this; @@ -73,9 +75,8 @@ public JacksonSerializableDatabase load(InputStream inputStream) throws IOExcept @Override public void save(OutputStream outputStream) { try { - SimpleModule module = new SimpleModule(); - module.addSerializer(PropertyValue.class, new ValueSerializer(encryptor)); + module.addSerializer(PropertyValue.class, new ValueSerializer(encryptor, propertyValueStrategy)); // disable auto-detection, only use annotated values XmlMapper mapper = XmlMapper.builder() .disable(MapperFeature.AUTO_DETECT_CREATORS, @@ -185,4 +186,12 @@ public void addBinary(int index, byte[] payload) { addBinary(keePassFile, index, payload); } + public PropertyValue.Strategy getPropertyValueStrategy() { + return propertyValueStrategy; + } + + public void setPropertyValueStrategy(PropertyValue.Strategy propertyValueStrategy) { + this.propertyValueStrategy = propertyValueStrategy; + } + } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java index 99049796..3e25b25c 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.java @@ -29,10 +29,12 @@ public class ValueSerializer extends StdSerializer { private final StreamEncryptor encryptor; + private final PropertyValue.Strategy propertyValueStrategy; - public ValueSerializer(StreamEncryptor encryptor) { + public ValueSerializer(StreamEncryptor encryptor, PropertyValue.Strategy propertyValueStrategy) { super(ValueSerializer.class, false); this.encryptor = encryptor; + this.propertyValueStrategy = propertyValueStrategy; } private String encrypt(byte[] bytes) { diff --git a/readme.md b/readme.md index 75c72284..fa4bcaca 100644 --- a/readme.md +++ b/readme.md @@ -188,7 +188,7 @@ only, with a view to being withdrawn, since they perform badly and/or depend on existed in the original database then this is the one. Aside from dependencies on underlying frameworks, different implementations have varying characteristics, primarily speed. This is assessed -by [this test](https://github.com/jorabin/KeePassJava2/blob/master/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java) in the module `examples`; +by [this test](https://github.com/jorabin/KeePassJava2/blob/master/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java) in the module `examples`. ## Dependencies From 0918cd9bf55c1c5ec7df8d9184b6a03326a9474c Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 13 Sep 2024 13:26:56 +0100 Subject: [PATCH 197/228] Undo accidental rename of Entry#setProperty --- .../main/java/org/linguafranca/pwdb/Entry.java | 2 +- .../linguafranca/pwdb/base/AbstractDatabase.java | 2 +- .../linguafranca/pwdb/base/AbstractEntry.java | 10 +++++----- .../pwdb/kdbx/dom/DomEntryWrapper.java | 2 +- .../pwdb/kdbx/jackson/JacksonEntry.java | 2 +- .../linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java | 2 +- .../java/org/linguafranca/pwdb/kdb/KdbEntry.java | 2 +- .../pwdb/kdbx/simple/SimpleEntry.java | 2 +- .../pwdb/checks/BasicDatabaseChecks.java | 16 ++++++++-------- .../pwdb/checks/SaveAndReloadChecks.java | 4 ++-- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/database/src/main/java/org/linguafranca/pwdb/Entry.java b/database/src/main/java/org/linguafranca/pwdb/Entry.java index 1818eb28..2c2a95a3 100644 --- a/database/src/main/java/org/linguafranca/pwdb/Entry.java +++ b/database/src/main/java/org/linguafranca/pwdb/Entry.java @@ -129,7 +129,7 @@ interface Matcher { * non-standard properties are not supported * @see Database#supportsNonStandardPropertyNames() */ - void setPropertyValue(String name, String value); + void setProperty(String name, String value); /** * Sets the value of a property as a property value. The method does not check whether the diff --git a/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java b/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java index 7e17721c..e8cada25 100644 --- a/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java +++ b/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java @@ -107,7 +107,7 @@ public E newEntry(Entry entry) { for (String propertyName: entry.getPropertyNames()) { try { // all implementations must support setting of STANDARD_PROPERTY_NAMES - result.setPropertyValue(propertyName, entry.getProperty(propertyName)); + result.setProperty(propertyName, entry.getProperty(propertyName)); } catch (UnsupportedOperationException e) { // oh well, we tried } diff --git a/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java b/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java index 50987d42..47f233d2 100644 --- a/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java +++ b/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java @@ -76,7 +76,7 @@ public String getUsername() { @Override public void setUsername(String username) { - setPropertyValue(STANDARD_PROPERTY_NAME_USER_NAME, username); + setProperty(STANDARD_PROPERTY_NAME_USER_NAME, username); touch(); } @@ -87,7 +87,7 @@ public String getPassword() { @Override public void setPassword(String pass) { - setPropertyValue(STANDARD_PROPERTY_NAME_PASSWORD, pass); + setProperty(STANDARD_PROPERTY_NAME_PASSWORD, pass); touch(); } @@ -98,7 +98,7 @@ public String getUrl() { @Override public void setUrl(String url) { - setPropertyValue(STANDARD_PROPERTY_NAME_URL, url); + setProperty(STANDARD_PROPERTY_NAME_URL, url); touch(); } @@ -109,7 +109,7 @@ public String getTitle() { @Override public void setTitle(String title) { - setPropertyValue(STANDARD_PROPERTY_NAME_TITLE, title); + setProperty(STANDARD_PROPERTY_NAME_TITLE, title); touch(); } @@ -120,7 +120,7 @@ public String getNotes() { @Override public void setNotes(String notes) { - setPropertyValue(STANDARD_PROPERTY_NAME_NOTES, notes); + setProperty(STANDARD_PROPERTY_NAME_NOTES, notes); touch(); } diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java index 293b41c8..39575301 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java +++ b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java @@ -68,7 +68,7 @@ public String getProperty(String name) { } @Override - public void setPropertyValue(String name, String value) { + public void setProperty(String name, String value) { Element property = DomHelper.getElement(String.format(DomHelper.PROPERTY_ELEMENT_FORMAT, name), element, false); if (property == null) { property = DomHelper.newElement("String", element); diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java index 897e7952..f6eb7c57 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java @@ -146,7 +146,7 @@ public String getProperty(String s) { @Override @JsonIgnore - public void setPropertyValue(String s, String s1) { + public void setProperty(String s, String s1) { StringProperty sp = getStringProperty(s, string); if (sp != null) { sp.setValue(database.getPropertyValueStrategy().newUnprotected().of(s1)); diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java index 1d4bea59..1eafe10d 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java @@ -82,7 +82,7 @@ public String getProperty(String name) { } @Override - public void setPropertyValue(String name, String value) { + public void setProperty(String name, String value) { StringField toRemove = null; for (StringField field: delegate.getString()){ if (field.getKey().equals(name)) { diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java index 15c32115..ac9927b7 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java @@ -62,7 +62,7 @@ public String getProperty(String name) { } @Override - public void setPropertyValue(String name, String value) { + public void setProperty(String name, String value) { switch (name) { case STANDARD_PROPERTY_NAME_USER_NAME: setUsername(value); break; case STANDARD_PROPERTY_NAME_PASSWORD: setPassword(value); break; diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java index ff76f94d..93f4f105 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java @@ -106,7 +106,7 @@ public String getProperty(String s) { } @Override - public void setPropertyValue(String s, String s1) { + public void setProperty(String s, String s1) { EntryClasses.StringProperty sp; if ((sp = getStringProperty(s, string)) != null) { this.string.remove(sp); diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java index 02764f41..dd0d06b5 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java @@ -135,19 +135,19 @@ public void testSetFields () { assertEquals(e1.getIcon(), ic1); // databases have to support setting of standard properties - e1.setPropertyValue(Entry.STANDARD_PROPERTY_NAME_TITLE, "A title"); + e1.setProperty(Entry.STANDARD_PROPERTY_NAME_TITLE, "A title"); assertEquals("A title", e1.getTitle()); - e1.setPropertyValue(Entry.STANDARD_PROPERTY_NAME_USER_NAME, "username"); + e1.setProperty(Entry.STANDARD_PROPERTY_NAME_USER_NAME, "username"); assertEquals("username", e1.getUsername()); - e1.setPropertyValue(Entry.STANDARD_PROPERTY_NAME_NOTES, "notes"); + e1.setProperty(Entry.STANDARD_PROPERTY_NAME_NOTES, "notes"); assertEquals("notes", e1.getNotes()); - e1.setPropertyValue(Entry.STANDARD_PROPERTY_NAME_PASSWORD, "password"); + e1.setProperty(Entry.STANDARD_PROPERTY_NAME_PASSWORD, "password"); assertEquals("password", e1.getPassword()); - e1.setPropertyValue(Entry.STANDARD_PROPERTY_NAME_URL, "url"); + e1.setProperty(Entry.STANDARD_PROPERTY_NAME_URL, "url"); assertEquals("url", e1.getUrl()); try { - e1.setPropertyValue("silly", "hello"); + e1.setProperty("silly", "hello"); assertEquals("hello", e1.getProperty("silly")); List properties = new ArrayList<>(Entry.STANDARD_PROPERTY_NAMES); properties.add("silly"); @@ -194,7 +194,7 @@ public void checkAddChangeRemoveProperty() { E entry = database.newEntry(); assertEquals(Entry.STANDARD_PROPERTY_NAMES.size(), entry.getPropertyNames().size()); try { - entry.setPropertyValue("test", "test1"); + entry.setProperty("test", "test1"); } catch (UnsupportedOperationException e) { if (!database.supportsNonStandardPropertyNames()) { return; @@ -202,7 +202,7 @@ public void checkAddChangeRemoveProperty() { fail("Database must report that it doesn't support non standrad properties"); } assertEquals("test1", entry.getProperty("test")); - entry.setPropertyValue("test", "test2"); + entry.setProperty("test", "test2"); assertEquals("test2", entry.getProperty("test")); assertTrue(entry.removeProperty("test")); assertFalse(entry.removeProperty("test")); diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java index 4e45886b..82c4733f 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java +++ b/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java @@ -265,9 +265,9 @@ public void testNewDatabase() throws IOException { assertEquals("A new entry", entry1.getTitle()); entry1.setUsername("user name"); assertEquals("user name", entry1.getUsername()); - entry1.setPropertyValue("random", "new"); + entry1.setProperty("random", "new"); assertEquals("new", entry1.getProperty("random")); - entry1.setPropertyValue("random", "old"); + entry1.setProperty("random", "old"); assertEquals("old", entry1.getProperty("random")); From 8699fa51a190e453f1593348dab31470d64cf57c Mon Sep 17 00:00:00 2001 From: Jo Date: Sat, 14 Sep 2024 01:36:31 +0100 Subject: [PATCH 198/228] Create dependabot.yml --- .github/dependabot.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..273fff0a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "maven" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" From 2c47bd7f1513353f1594b95534a91d24a3078088 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sat, 14 Sep 2024 12:59:15 +0100 Subject: [PATCH 199/228] Revert "Create dependabot.yml" This reverts commit 8699fa51a190e453f1593348dab31470d64cf57c. --- .github/dependabot.yml | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 273fff0a..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,11 +0,0 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file - -version: 2 -updates: - - package-ecosystem: "maven" # See documentation for possible values - directory: "/" # Location of package manifests - schedule: - interval: "weekly" From 83cb2593a33549835fabda1ac79d8c37841ae027 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 24 Sep 2024 21:42:38 +0100 Subject: [PATCH 200/228] Adding equals etc methods to AESKey --- .../org/linguafranca/pwdb/PropertyValue.java | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java b/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java index 07fe661c..221020ff 100644 --- a/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java +++ b/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java @@ -5,7 +5,6 @@ import javax.crypto.Cipher; import javax.crypto.SealedObject; import javax.crypto.SecretKey; -import javax.security.auth.DestroyFailedException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -19,11 +18,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; /** * An interface through which (textual) property values can be stored in memory as something other than String - * and using various techniques for obfuscating the value and to make it - * harder to access the values via a heap dump etc. + * and using various techniques for obfuscating the value and to make it harder to access the values + * via a heap dump etc. */ public interface PropertyValue { CharSequence getValue(); @@ -330,8 +330,10 @@ public static PropertyValue.Factory getFactory() { /** * Believe it or not ... a SecretKey generated by the standard KeyGenerator throws an * exception when the destroy() method is called, so we roll our own + *

    + * See JavaDoc of {@link SecretKey} re serialization etc */ - private static class AESKey implements SecretKey, Serializable { + private static class AESKey implements SecretKey { private boolean destroyed = false; private final byte[] key = new byte[16]; final String algorithm = "AES"; @@ -365,6 +367,29 @@ public String getFormat() { public byte[] getEncoded() { return this.key.clone(); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AESKey)) return false; + AESKey aesKey = (AESKey) o; + return destroyed == aesKey.destroyed && Arrays.equals(key, aesKey.key); + } + + @Override + public int hashCode() { + int result = Objects.hash(destroyed, algorithm); + result = 31 * result + Arrays.hashCode(key); + return result; + } + + @SuppressWarnings("unused") + public void writeObject(ObjectOutputStream os) throws IOException { + if (destroyed) { + throw new IllegalStateException("Can't serialize a destroyed key"); + } + os.defaultWriteObject(); + } } public SealedStore(byte [] bytes){ From 15a06b44170e63896ac91fad77e437fb040d7fc2 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Wed, 9 Oct 2024 16:25:05 +0100 Subject: [PATCH 201/228] tweaks to property value --- .../src/main/java/org/linguafranca/pwdb/PropertyValue.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java b/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java index 221020ff..19111bf3 100644 --- a/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java +++ b/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java @@ -130,7 +130,7 @@ public Factory newUnprotected() { class StringStore implements PropertyValue { private final String value; private final static PropertyValue.Factory factory = - new PropertyValue.Factory(){ + new PropertyValue.Factory<>(){ @Override public StringStore of(CharSequence aCharSequence) { @@ -195,7 +195,7 @@ class BytesStore implements PropertyValue, Serializable { private final byte[] value; private final static PropertyValue.Factory factory = - new PropertyValue.Factory(){ + new PropertyValue.Factory<>(){ @Override public BytesStore of(CharSequence aCharSequence) { @@ -305,7 +305,7 @@ private byte[] getBytes() { } private final static PropertyValue.Factory factory = - new PropertyValue.Factory(){ + new PropertyValue.Factory<>(){ @Override public SealedStore of(CharSequence aCharSequence) { From eb5fdc00269c0fc502b7c850cf5e6989f9d78c8f Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 14 Oct 2024 19:58:36 +0100 Subject: [PATCH 202/228] revert anonymous type / inner class for Java 8 --- .../src/main/java/org/linguafranca/pwdb/PropertyValue.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java b/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java index 19111bf3..221020ff 100644 --- a/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java +++ b/database/src/main/java/org/linguafranca/pwdb/PropertyValue.java @@ -130,7 +130,7 @@ public Factory newUnprotected() { class StringStore implements PropertyValue { private final String value; private final static PropertyValue.Factory factory = - new PropertyValue.Factory<>(){ + new PropertyValue.Factory(){ @Override public StringStore of(CharSequence aCharSequence) { @@ -195,7 +195,7 @@ class BytesStore implements PropertyValue, Serializable { private final byte[] value; private final static PropertyValue.Factory factory = - new PropertyValue.Factory<>(){ + new PropertyValue.Factory(){ @Override public BytesStore of(CharSequence aCharSequence) { @@ -305,7 +305,7 @@ private byte[] getBytes() { } private final static PropertyValue.Factory factory = - new PropertyValue.Factory<>(){ + new PropertyValue.Factory(){ @Override public SealedStore of(CharSequence aCharSequence) { From 8430070de5532cc2e8045117c4d535a2318976b2 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 19 Nov 2024 20:06:48 +0000 Subject: [PATCH 203/228] Addressing #71 --- database/pom.xml | 11 +- .../linguafranca/pwdb/PropertyValueTest.java | 28 +- dom/pom.xml | 12 +- .../pwdb/kdbx/dom/DomDatabaseWrapperTest.java | 3 +- .../pwdb/kdbx/dom/DomHelperTest.java | 2 +- .../pwdb/kdbx/dom/DomPropertyValueTest.java | 0 .../pwdb/kdbx/dom/DomSaveAndReloadTest.java | 1 - .../linguafranca/pwdb/kdbx/dom/DomV4Test.java | 2 +- .../pwdb/kdbx/dom/VisitorTest.java | 2 +- example/pom.xml | 11 +- .../linguafranca/pwdb/kdbx/ChooseFile.java | 3 +- .../linguafranca/pwdb/kdbx/OpenDbExample.java | 2 +- .../linguafranca/pwdb/kdbx/QuickStart.java | 5 +- .../linguafranca/pwdb/kdbx/SaxParseTest.java | 2 +- .../org/linguafranca/pwdb/kdbx/UtilTest.java | 2 +- .../pwdb/kdbx/validation/Issue27Test.java | 2 +- .../pwdb/kdbx/validation/Issue38Test.java | 2 +- jackson/pom.xml | 13 +- .../pwdb/JacksonDatabaseLoadTest.java | 2 +- .../pwdb/JacksonPropertyValueTest.java | 1 - .../pwdb/JacksonSaveAndReloadTest.java | 8 - jaxb/pom.xml | 18 +- .../jaxb/JaxbSerializableDatabaseTest.java | 2 +- .../linguafranca/pwdb/kdbx/jaxb/JaxbTest.java | 2 +- .../pwdb/kdbx/jaxb/JaxbV4LoadTest.java | 2 +- kdb/pom.xml | 8 +- .../pwdb/kdb/KdbSerializerTest.java | 2 +- kdbx/pom.xml | 11 +- .../pwdb/hashedblock/HashBlockTest.java | 2 +- .../linguafranca/pwdb/kdbx/HelperTest.java | 2 +- .../linguafranca/pwdb/kdbx/HelpersTest.java | 2 +- .../pwdb/kdbx/KdbxHeaderTest.java | 2 +- .../pwdb/kdbx/KdbxKeyFileTest.java | 2 +- .../pwdb/kdbx/KdbxSerializerTest.java | 2 +- .../pwdb/security/StreamEncryptionTest.java | 2 +- pom.xml | 22 +- simple/pom.xml | 6 +- .../kdbx/simple/SimpleDatabaseLoadTest.java | 3 +- .../kdbx/simple/SimpleSaveAndReloadTest.java | 2 +- test/pom.xml | 22 + .../java/org/linguafranca/util/TestUtil.java | 17 - .../pwdb/checks/BasicDatabaseChecks.java | 0 .../pwdb/checks/BinaryPropertyChecks.java | 0 .../pwdb/checks/DatabaseLoaderChecks.java | 2 +- .../pwdb/checks/PropertyValueChecks.java | 0 .../pwdb/checks/RecycleBinChecks.java | 0 .../pwdb/checks/SaveAndReloadChecks.java | 2 +- .../{main => test}/resources/Attachment.kdbx | Bin .../{main => test}/resources/Attachment.xml | 312 ++--- .../resources/EmptyPassword.kdbx | Bin .../resources/EmptyPasswordWithKey.kdbx | Bin .../resources/EmptyPasswordWithKey.key | 0 .../resources/ExampleDatabase.kdbx | Bin .../resources/ExampleDatabase.xml | 1164 ++++++++--------- .../resources/KeyFileDatabase.kdbx | Bin .../resources/KeyFileDatabase.key | 18 +- .../resources/NoPasswordWithKey.kdbx | Bin .../resources/NoPasswordWithKey.key | 18 +- .../resources/V3-CustomIcon.kdbx | Bin .../{main => test}/resources/V4-AES-AES.kdbx | Bin .../resources/V4-AES-Argon2-CustomIcon.kdbx | Bin .../resources/V4-AES-Argon2.kdbx | Bin .../resources/V4-ChaCha20-AES.kdbx | Bin .../V4-ChaCha20-Argon2-Attachment.kdbx | Bin .../resources/V4-CustomIcon.kdbx | Bin .../resources/issue-27/bogus-timestamp.kdbx | Bin .../resources/issue-27/bogus-timestamp2.kdbx | Bin .../resources/issue-38/Database/Database.kdbx | Bin .../resources/issue-38/Database/Database.keyx | 0 .../resources/issue-38/Database/README.md | 0 test/src/{main => test}/resources/kdb.key | 0 .../resources/kdb_with_random_file.kdbx | Bin .../{main => test}/resources/kdbwithkey.kdb | Bin .../{main => test}/resources/kdbxSample.xml | 0 .../resources/kdbx_hash_test.kdbx | Bin .../resources/kdbx_hash_test.keyx | 22 +- .../resources/kdbx_hash_test_wrong_hash.keyx | 22 +- .../resources/kdbx_keyfile32.kdbx | Bin .../resources/kdbx_keyfile64.kdbx | Bin test/src/{main => test}/resources/keyfile32 | 0 test/src/{main => test}/resources/keyfile64 | 0 .../{main => test}/resources/letter J.jpeg | Bin .../{main => test}/resources/letter L.jpeg | Bin test/src/{main => test}/resources/random_file | Bin test/src/{main => test}/resources/test1.kdbx | Bin .../resources/test123-AES-AES.kdbx | Bin .../resources/test123-AES-Argon2.kdbx | Bin .../resources/test123-ChaCha20-AES.kdbx | Bin .../resources/test123-ChaCha20-Argon2.kdbx | Bin test/src/{main => test}/resources/test123.kdb | Bin .../src/{main => test}/resources/test123.kdbx | Bin .../{main => test}/resources/testDeleted.kdbx | Bin .../resources/xml/V4-AES-AES.xml | 0 .../resources/xml/V4-AES-Argon2.xml | 0 .../resources/xml/V4-ChaCha20-AES.xml | 0 .../xml/V4-ChaCha20-Argon2-Attachment.xml | 0 util/pom.xml | 29 + .../test/maven}/MavenRunListener.java | 2 +- .../linguafranca/test}/util/HexViewer.java | 12 +- .../org/linguafranca/test/util/TestUtil.java | 27 + 100 files changed, 945 insertions(+), 930 deletions(-) rename dom/src/{main => test}/java/org/linguafranca/pwdb/kdbx/dom/DomPropertyValueTest.java (100%) delete mode 100644 test/src/main/java/org/linguafranca/util/TestUtil.java rename test/src/{main => test}/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java (100%) rename test/src/{main => test}/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java (100%) rename test/src/{main => test}/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java (98%) rename test/src/{main => test}/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java (100%) rename test/src/{main => test}/java/org/linguafranca/pwdb/checks/RecycleBinChecks.java (100%) rename test/src/{main => test}/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java (99%) rename test/src/{main => test}/resources/Attachment.kdbx (100%) rename test/src/{main => test}/resources/Attachment.xml (99%) rename test/src/{main => test}/resources/EmptyPassword.kdbx (100%) rename test/src/{main => test}/resources/EmptyPasswordWithKey.kdbx (100%) rename test/src/{main => test}/resources/EmptyPasswordWithKey.key (100%) rename test/src/{main => test}/resources/ExampleDatabase.kdbx (100%) rename test/src/{main => test}/resources/ExampleDatabase.xml (99%) rename test/src/{main => test}/resources/KeyFileDatabase.kdbx (100%) rename test/src/{main => test}/resources/KeyFileDatabase.key (95%) rename test/src/{main => test}/resources/NoPasswordWithKey.kdbx (100%) rename test/src/{main => test}/resources/NoPasswordWithKey.key (95%) rename test/src/{main => test}/resources/V3-CustomIcon.kdbx (100%) rename test/src/{main => test}/resources/V4-AES-AES.kdbx (100%) rename test/src/{main => test}/resources/V4-AES-Argon2-CustomIcon.kdbx (100%) rename test/src/{main => test}/resources/V4-AES-Argon2.kdbx (100%) rename test/src/{main => test}/resources/V4-ChaCha20-AES.kdbx (100%) rename test/src/{main => test}/resources/V4-ChaCha20-Argon2-Attachment.kdbx (100%) rename test/src/{main => test}/resources/V4-CustomIcon.kdbx (100%) rename test/src/{main => test}/resources/issue-27/bogus-timestamp.kdbx (100%) rename test/src/{main => test}/resources/issue-27/bogus-timestamp2.kdbx (100%) rename test/src/{main => test}/resources/issue-38/Database/Database.kdbx (100%) rename test/src/{main => test}/resources/issue-38/Database/Database.keyx (100%) rename test/src/{main => test}/resources/issue-38/Database/README.md (100%) rename test/src/{main => test}/resources/kdb.key (100%) rename test/src/{main => test}/resources/kdb_with_random_file.kdbx (100%) rename test/src/{main => test}/resources/kdbwithkey.kdb (100%) rename test/src/{main => test}/resources/kdbxSample.xml (100%) rename test/src/{main => test}/resources/kdbx_hash_test.kdbx (100%) rename test/src/{main => test}/resources/kdbx_hash_test.keyx (95%) rename test/src/{main => test}/resources/kdbx_hash_test_wrong_hash.keyx (95%) rename test/src/{main => test}/resources/kdbx_keyfile32.kdbx (100%) rename test/src/{main => test}/resources/kdbx_keyfile64.kdbx (100%) rename test/src/{main => test}/resources/keyfile32 (100%) rename test/src/{main => test}/resources/keyfile64 (100%) rename test/src/{main => test}/resources/letter J.jpeg (100%) rename test/src/{main => test}/resources/letter L.jpeg (100%) rename test/src/{main => test}/resources/random_file (100%) rename test/src/{main => test}/resources/test1.kdbx (100%) rename test/src/{main => test}/resources/test123-AES-AES.kdbx (100%) rename test/src/{main => test}/resources/test123-AES-Argon2.kdbx (100%) rename test/src/{main => test}/resources/test123-ChaCha20-AES.kdbx (100%) rename test/src/{main => test}/resources/test123-ChaCha20-Argon2.kdbx (100%) rename test/src/{main => test}/resources/test123.kdb (100%) rename test/src/{main => test}/resources/test123.kdbx (100%) rename test/src/{main => test}/resources/testDeleted.kdbx (100%) rename test/src/{main => test}/resources/xml/V4-AES-AES.xml (100%) rename test/src/{main => test}/resources/xml/V4-AES-Argon2.xml (100%) rename test/src/{main => test}/resources/xml/V4-ChaCha20-AES.xml (100%) rename test/src/{main => test}/resources/xml/V4-ChaCha20-Argon2-Attachment.xml (100%) create mode 100644 util/pom.xml rename {database/src/main/java/org/linguafranca/util => util/src/main/java/org/linguafranca/test/maven}/MavenRunListener.java (98%) rename {test/src/main/java/org/linguafranca => util/src/main/java/org/linguafranca/test}/util/HexViewer.java (70%) create mode 100644 util/src/main/java/org/linguafranca/test/util/TestUtil.java diff --git a/database/pom.xml b/database/pom.xml index f4095dec..55c6a466 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -13,19 +13,18 @@ Contains root definitions of PWDB Databases etc. - - org.jetbrains - annotations - 24.1.0 - com.google.guava guava - 33.2.1-android org.bouncycastle bcpkix-jdk18on + + org.linguafranca.pwdb + util + ${project.version} + \ No newline at end of file diff --git a/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java b/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java index 21918401..39e55a57 100644 --- a/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java +++ b/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java @@ -1,10 +1,9 @@ package org.linguafranca.pwdb; import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.nio.charset.StandardCharsets; +import org.linguafranca.test.util.TestUtil; import static org.junit.Assert.*; @@ -12,7 +11,10 @@ public class PropertyValueTest { public static final String THIS_IS_A_SECRET = "This is a secret + לַחַיִּים"; public static final String ANOTHER_SECRET = "password with accents àéç"; - Logger logger = LoggerFactory.getLogger(PropertyValueTest.class); + + static { + TestUtil.getTestPrintStream().println("I am only here for the dependency"); + } @Test public void bytesTest() { @@ -31,7 +33,7 @@ public void bytesTest() { assertArrayEquals(THIS_IS_A_SECRET.toCharArray(), testValue.getValueAsChars()); assertArrayEquals(THIS_IS_A_SECRET.getBytes(StandardCharsets.UTF_8), testValue.getValueAsBytes()); // test as byte[] - testValue = factory.of(THIS_IS_A_SECRET.getBytes()); + testValue = factory.of(THIS_IS_A_SECRET.getBytes(StandardCharsets.UTF_8)); assertEquals(THIS_IS_A_SECRET, testValue.getValue().toString()); assertEquals(THIS_IS_A_SECRET, testValue.getValueAsString()); assertArrayEquals(THIS_IS_A_SECRET.toCharArray(), testValue.getValueAsChars()); @@ -44,19 +46,19 @@ public void sealedObjectTest() { PropertyValue.SealedStore sealed = factory.of(THIS_IS_A_SECRET); assertEquals(THIS_IS_A_SECRET, sealed.getValue().toString()); - assertArrayEquals(THIS_IS_A_SECRET.getBytes(), sealed.getValueAsBytes()); + assertArrayEquals(THIS_IS_A_SECRET.getBytes(StandardCharsets.UTF_8), sealed.getValueAsBytes()); assertArrayEquals(THIS_IS_A_SECRET.toCharArray(), sealed.getValueAsChars()); assertEquals(THIS_IS_A_SECRET, sealed.getValueAsString()); - sealed = factory.of(THIS_IS_A_SECRET.getBytes()); + sealed = factory.of(THIS_IS_A_SECRET.getBytes(StandardCharsets.UTF_8)); assertEquals(THIS_IS_A_SECRET, sealed.getValue().toString()); - assertArrayEquals(THIS_IS_A_SECRET.getBytes(), sealed.getValueAsBytes()); + assertArrayEquals(THIS_IS_A_SECRET.getBytes(StandardCharsets.UTF_8), sealed.getValueAsBytes()); assertArrayEquals(THIS_IS_A_SECRET.toCharArray(), sealed.getValueAsChars()); assertEquals(THIS_IS_A_SECRET, sealed.getValueAsString()); sealed =factory.of(THIS_IS_A_SECRET.toCharArray()); assertEquals(THIS_IS_A_SECRET, sealed.getValue().toString()); - assertArrayEquals(THIS_IS_A_SECRET.getBytes(), sealed.getValueAsBytes()); + assertArrayEquals(THIS_IS_A_SECRET.getBytes(StandardCharsets.UTF_8), sealed.getValueAsBytes()); assertArrayEquals(THIS_IS_A_SECRET.toCharArray(), sealed.getValueAsChars()); assertEquals(THIS_IS_A_SECRET, sealed.getValueAsString()); } @@ -66,19 +68,19 @@ public void stringTest() { PropertyValue.StringStore stringStore = factory.of(THIS_IS_A_SECRET); assertEquals(THIS_IS_A_SECRET, stringStore.getValue()); - assertArrayEquals(THIS_IS_A_SECRET.getBytes(), stringStore.getValueAsBytes()); + assertArrayEquals(THIS_IS_A_SECRET.getBytes(StandardCharsets.UTF_8), stringStore.getValueAsBytes()); assertArrayEquals(THIS_IS_A_SECRET.toCharArray(), stringStore.getValueAsChars()); assertEquals(THIS_IS_A_SECRET, stringStore.getValueAsString()); - stringStore = factory.of(THIS_IS_A_SECRET.getBytes()); + stringStore = factory.of(THIS_IS_A_SECRET.getBytes(StandardCharsets.UTF_8)); assertEquals(THIS_IS_A_SECRET, stringStore.getValue()); - assertArrayEquals(THIS_IS_A_SECRET.getBytes(), stringStore.getValueAsBytes()); + assertArrayEquals(THIS_IS_A_SECRET.getBytes(StandardCharsets.UTF_8), stringStore.getValueAsBytes()); assertArrayEquals(THIS_IS_A_SECRET.toCharArray(), stringStore.getValueAsChars()); assertEquals(THIS_IS_A_SECRET, stringStore.getValueAsString()); stringStore =factory.of(THIS_IS_A_SECRET.toCharArray()); assertEquals(THIS_IS_A_SECRET, stringStore.getValue()); - assertArrayEquals(THIS_IS_A_SECRET.getBytes(), stringStore.getValueAsBytes()); + assertArrayEquals(THIS_IS_A_SECRET.getBytes(StandardCharsets.UTF_8), stringStore.getValueAsBytes()); assertArrayEquals(THIS_IS_A_SECRET.toCharArray(), stringStore.getValueAsChars()); assertEquals(THIS_IS_A_SECRET, stringStore.getValueAsString()); } @@ -102,7 +104,7 @@ public void getCharsTest(){ byte [] bytes = cs.getValueAsBytes(); bytes [0] = 0; // same for bytes - assertArrayEquals("a test".getBytes(), cs.getValueAsBytes()); + assertArrayEquals("a test".getBytes(StandardCharsets.UTF_8), cs.getValueAsBytes()); } @Test diff --git a/dom/pom.xml b/dom/pom.xml index fa44bcb4..dbc81766 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -21,18 +21,8 @@ org.linguafranca.pwdb test ${project.version} + test-jar test - - junit - junit - test - - - org.linguafranca.pwdb - test - 2.2.3-SNAPSHOT - compile - \ No newline at end of file diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java index aef78d3d..4459274a 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java @@ -20,7 +20,6 @@ import org.junit.Test; import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.kdbx.KdbxCreds; -import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.Credentials; @@ -28,7 +27,7 @@ import java.io.InputStream; import java.io.PrintStream; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomHelperTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomHelperTest.java index 227b5e3e..e77c183f 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomHelperTest.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomHelperTest.java @@ -22,7 +22,7 @@ import java.io.PrintStream; import static org.junit.Assert.assertEquals; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomPropertyValueTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomPropertyValueTest.java similarity index 100% rename from dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomPropertyValueTest.java rename to dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomPropertyValueTest.java diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomSaveAndReloadTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomSaveAndReloadTest.java index 704f0a72..e4c9b68e 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomSaveAndReloadTest.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomSaveAndReloadTest.java @@ -22,7 +22,6 @@ import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.kdbx.KdbxHeader; -import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import java.io.IOException; import java.io.InputStream; diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomV4Test.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomV4Test.java index d3ad03a2..95345b3b 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomV4Test.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomV4Test.java @@ -7,7 +7,7 @@ import java.io.InputStream; import java.io.PrintStream; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo diff --git a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/VisitorTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/VisitorTest.java index 199c78b9..1e34e0d4 100644 --- a/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/VisitorTest.java +++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/VisitorTest.java @@ -30,7 +30,7 @@ import java.util.List; import static org.junit.Assert.assertTrue; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author Jo diff --git a/example/pom.xml b/example/pom.xml index 7cfde215..27f5e5e8 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -38,16 +38,17 @@ KeePassJava2 ${project.version} - - org.linguafranca.pwdb - test - ${project.version} - io.github.novacrypto SecureString 2022.01.17 + + org.linguafranca.pwdb + test + ${project.version} + test-jar + diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java index e0f4f266..d8645fda 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java @@ -1,7 +1,6 @@ package org.linguafranca.pwdb.kdbx; import com.google.common.base.Strings; -import org.linguafranca.util.HexViewer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,7 +11,7 @@ import java.nio.file.Files; import java.nio.file.Paths; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * Utility to allow browsing of database files and listing content to console diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java index 8af6c6f3..e24f4e4f 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java @@ -10,7 +10,7 @@ import java.io.InputStream; import java.io.PrintStream; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * Example and naive speed test for various implementations. diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java index c68a93b3..3ea075f5 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java @@ -16,7 +16,6 @@ package org.linguafranca.pwdb.kdbx; -import org.junit.BeforeClass; import org.linguafranca.pwdb.*; import org.linguafranca.pwdb.kdb.KdbCredentials; import org.linguafranca.pwdb.kdb.KdbDatabase; @@ -28,8 +27,7 @@ import java.nio.file.Paths; import java.util.List; -import static org.junit.Assert.assertEquals; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * Examples for QuickStart @@ -45,7 +43,6 @@ public abstract class QuickStart, G extends Group public abstract D getDatabase(); public abstract D loadDatabase(Credentials creds, InputStream inputStream); - @BeforeClass public static void ensureOutputDir() throws IOException { Files.createDirectories(Paths.get("testOutput")); } diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/SaxParseTest.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/SaxParseTest.java index 1d673046..741036f7 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/SaxParseTest.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/SaxParseTest.java @@ -24,7 +24,7 @@ import java.io.OutputStream; import java.io.PrintWriter; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java index bfda2c5b..8c88280b 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java @@ -7,7 +7,7 @@ import java.io.PrintWriter; import static org.linguafranca.pwdb.kdbx.Util.*; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; public class UtilTest { diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue27Test.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue27Test.java index be04ad11..55407ba4 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue27Test.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue27Test.java @@ -18,7 +18,7 @@ import java.util.List; import static org.junit.Assert.assertEquals; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; public class Issue27Test { static PrintStream printStream = getTestPrintStream(); diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue38Test.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue38Test.java index 4684a712..9cc87ac8 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue38Test.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue38Test.java @@ -10,7 +10,7 @@ import java.io.PrintStream; import java.util.List; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * Bug report on GitHub, the Keyfile is Version 2 (Hex) diff --git a/jackson/pom.xml b/jackson/pom.xml index 9726b910..b2a96012 100644 --- a/jackson/pom.xml +++ b/jackson/pom.xml @@ -33,6 +33,13 @@ KeePassJava2-kdbx ${project.version} + + org.linguafranca.pwdb + test + ${project.version} + test-jar + test + com.fasterxml.jackson.dataformat jackson-dataformat-xml @@ -43,11 +50,5 @@ woodstox-core 7.0.0 - - org.linguafranca.pwdb - test - ${project.version} - test - diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoadTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoadTest.java index 2f0e4d2e..06e40d78 100644 --- a/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoadTest.java +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoadTest.java @@ -22,7 +22,7 @@ import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; public class JacksonDatabaseLoadTest { diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java index 40571e53..d7e29b0d 100644 --- a/jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java @@ -18,7 +18,6 @@ import org.linguafranca.pwdb.checks.BasicDatabaseChecks; import org.linguafranca.pwdb.checks.PropertyValueChecks; import org.linguafranca.pwdb.kdbx.KdbxCreds; -import org.linguafranca.pwdb.kdbx.KdbxHeader; import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; import org.linguafranca.pwdb.kdbx.jackson.JacksonEntry; import org.linguafranca.pwdb.kdbx.jackson.JacksonGroup; diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonSaveAndReloadTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonSaveAndReloadTest.java index fcb2db76..8e09395e 100644 --- a/jackson/src/test/java/org/linguafranca/pwdb/JacksonSaveAndReloadTest.java +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonSaveAndReloadTest.java @@ -16,19 +16,11 @@ package org.linguafranca.pwdb; import static org.junit.Assert.assertTrue; -import static org.linguafranca.util.TestUtil.getTestPrintStream; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; -import java.io.PrintStream; -import org.junit.Test; import org.linguafranca.pwdb.checks.SaveAndReloadChecks; import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.kdbx.KdbxHeader; diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 535b53de..a8dc4d3d 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -34,6 +34,13 @@ KeePassJava2-kdbx ${project.version} + + org.linguafranca.pwdb + test + ${project.version} + test-jar + test + javax.xml.bind @@ -45,17 +52,6 @@ jaxb-impl 2.3.9 - - org.linguafranca.pwdb - test - ${project.version} - test - - - junit - junit - test - diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabaseTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabaseTest.java index 97921e51..420630a8 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabaseTest.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabaseTest.java @@ -24,7 +24,7 @@ import java.io.InputStream; import java.io.PrintStream; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbTest.java index 60aa341d..f1e47dfa 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbTest.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbTest.java @@ -23,7 +23,7 @@ import javax.xml.bind.*; import java.io.PrintStream; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbV4LoadTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbV4LoadTest.java index 66ecd37e..be1f8695 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbV4LoadTest.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbV4LoadTest.java @@ -8,7 +8,7 @@ import java.io.InputStream; import java.io.PrintStream; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo diff --git a/kdb/pom.xml b/kdb/pom.xml index f78d55f9..07672354 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -10,7 +10,7 @@ KeePassJava2-kdb KeePassJava2 :: KDB - Contains an implementaion that can load KDB files. + Contains an implementation that can load KDB files. @@ -18,15 +18,11 @@ database ${project.version} - - org.jetbrains - annotations - 24.1.0 - org.linguafranca.pwdb test ${project.version} + test-jar test diff --git a/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbSerializerTest.java b/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbSerializerTest.java index 06254062..72e426dd 100644 --- a/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbSerializerTest.java +++ b/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbSerializerTest.java @@ -23,7 +23,7 @@ import java.io.InputStream; import java.io.PrintStream; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo diff --git a/kdbx/pom.xml b/kdbx/pom.xml index 282eeed4..d66e028c 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -43,20 +43,11 @@ commons-codec 1.17.0 - - org.jetbrains - annotations - 24.1.0 - org.linguafranca.pwdb test ${project.version} - test - - - junit - junit + test-jar test diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/hashedblock/HashBlockTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/hashedblock/HashBlockTest.java index 14d21611..8e16529f 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/hashedblock/HashBlockTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/hashedblock/HashBlockTest.java @@ -22,7 +22,7 @@ import java.util.Arrays; import static org.junit.Assert.*; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelperTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelperTest.java index 586bcf81..3a14a87f 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelperTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelperTest.java @@ -22,7 +22,7 @@ import java.util.UUID; import static org.junit.Assert.assertEquals; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * tests the operation of Hex conversion diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelpersTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelpersTest.java index c4ad11aa..033cb2ab 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelpersTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelpersTest.java @@ -11,7 +11,7 @@ import static org.junit.Assert.*; import static org.linguafranca.pwdb.kdbx.Helpers.dateTimeFormatter; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; public class HelpersTest { diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxHeaderTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxHeaderTest.java index f6a3b440..45946287 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxHeaderTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxHeaderTest.java @@ -16,7 +16,7 @@ import java.nio.charset.StandardCharsets; import static org.junit.Assert.assertEquals; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * test decryption of various kinds diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java index 31b8801f..9bb6c352 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java @@ -28,7 +28,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * tests reading of kdbx with various combinations of key file and password diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java index 27d8011d..14ccd4c2 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java @@ -25,7 +25,7 @@ import java.util.Scanner; import static org.junit.Assert.assertEquals; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * test operation of kdbx diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java index 30a77ed6..c3fbee0f 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java @@ -23,7 +23,7 @@ import java.security.SecureRandom; import static org.junit.Assert.assertEquals; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * Verify that the inner random encryption works two-way diff --git a/pom.xml b/pom.xml index e74cd2b5..f064f123 100644 --- a/pom.xml +++ b/pom.xml @@ -32,6 +32,7 @@ jackson all example + util pom @@ -102,7 +103,7 @@ listener - org.linguafranca.util.MavenRunListener + org.linguafranca.test.maven.MavenRunListener @@ -152,19 +153,24 @@ - - junit - junit - org.slf4j slf4j-api - 2.0.12 ch.qos.logback logback-classic + + org.jetbrains + annotations + provided + + + junit + junit + test + @@ -172,7 +178,7 @@ org.jetbrains annotations - 24.0.1 + 24.1.0 @@ -188,7 +194,7 @@ org.slf4j slf4j-api - 2.0.5 + 2.0.12 diff --git a/simple/pom.xml b/simple/pom.xml index 6e8e5078..535242f4 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -38,11 +38,7 @@ org.linguafranca.pwdb test ${project.version} - test - - - junit - junit + test-jar test diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java index 8feb77c5..d6250be4 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java @@ -19,14 +19,13 @@ import org.junit.Ignore; import org.junit.Test; import org.linguafranca.pwdb.Entry; -import org.linguafranca.pwdb.Group; import org.linguafranca.pwdb.Visitor; import org.linguafranca.pwdb.kdbx.KdbxCreds; import java.io.InputStream; import java.io.PrintStream; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java index d848e759..ab394211 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java @@ -28,7 +28,7 @@ import java.io.*; import static org.junit.Assert.assertTrue; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo diff --git a/test/pom.xml b/test/pom.xml index 529ff049..e9cc213a 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -17,6 +17,28 @@ database ${project.version} + + org.linguafranca.pwdb + util + 2.2.3-SNAPSHOT + test + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.2 + + + + test-jar + + + + + + \ No newline at end of file diff --git a/test/src/main/java/org/linguafranca/util/TestUtil.java b/test/src/main/java/org/linguafranca/util/TestUtil.java deleted file mode 100644 index c6433e42..00000000 --- a/test/src/main/java/org/linguafranca/util/TestUtil.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.linguafranca.util; - -import com.google.common.io.ByteStreams; - -import java.io.PrintStream; - -public class TestUtil { - /** - * set system property to suppress output from tests - * @return if "nullOutput has been set, e.g. in a profile - */ - public static PrintStream getTestPrintStream() { - return Boolean.getBoolean("inhibitConsoleOutput") ? - new PrintStream(ByteStreams.nullOutputStream()) : - new PrintStream(System.out); - } -} diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java b/test/src/test/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java similarity index 100% rename from test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java rename to test/src/test/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java b/test/src/test/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java similarity index 100% rename from test/src/main/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java rename to test/src/test/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java b/test/src/test/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java similarity index 98% rename from test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java rename to test/src/test/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java index 140ea1c1..32de1949 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java +++ b/test/src/test/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java @@ -27,7 +27,7 @@ import java.util.List; import static org.junit.Assert.assertEquals; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java b/test/src/test/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java similarity index 100% rename from test/src/main/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java rename to test/src/test/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/RecycleBinChecks.java b/test/src/test/java/org/linguafranca/pwdb/checks/RecycleBinChecks.java similarity index 100% rename from test/src/main/java/org/linguafranca/pwdb/checks/RecycleBinChecks.java rename to test/src/test/java/org/linguafranca/pwdb/checks/RecycleBinChecks.java diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java b/test/src/test/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java similarity index 99% rename from test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java rename to test/src/test/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java index 82c4733f..a0e05a51 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java +++ b/test/src/test/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java @@ -28,7 +28,7 @@ import java.util.UUID; import static org.junit.Assert.*; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo diff --git a/test/src/main/resources/Attachment.kdbx b/test/src/test/resources/Attachment.kdbx similarity index 100% rename from test/src/main/resources/Attachment.kdbx rename to test/src/test/resources/Attachment.kdbx diff --git a/test/src/main/resources/Attachment.xml b/test/src/test/resources/Attachment.xml similarity index 99% rename from test/src/main/resources/Attachment.xml rename to test/src/test/resources/Attachment.xml index e400d526..5bdd5620 100755 --- a/test/src/main/resources/Attachment.xml +++ b/test/src/test/resources/Attachment.xml @@ -1,157 +1,157 @@ - - - - KeePass - - 2016-08-27T14:41:45Z - - 2016-08-27T14:41:45Z - - 2016-08-27T14:41:45Z - 365 - - 2016-08-27T14:41:58Z - -1 - -1 - - False - False - True - False - False - - True - AAAAAAAAAAAAAAAAAAAAAA== - 2016-08-27T14:41:45Z - AAAAAAAAAAAAAAAAAAAAAA== - 2016-08-27T14:41:45Z - 10 - 6291456 - AAAAAAAAAAAAAAAAAAAAAA== - AAAAAAAAAAAAAAAAAAAAAA== - - H4sIAAAAAAAEAJWXZ1AT3rbFEwIEQldAgRgUIr0IUkOLhCqG3omIAlJDb6EIf1CB0KRLE5TQe0cUpAgoAZUaCb0I0kR6l4f3znvz7se79/q2ZvZvzTkz58w+/34+C2C+q6GtAQACAQDgRQPOJwBPAbTUYBowNS0NmAYCoaVjYGdioKdn4GJlY2a/fo2H5/o1bm64gJQInE+Cn5tbVElUQloWgUDwCquoqcihpOQRskAIBMJAz8DJxMQpe5P7pux/XeftABYaAA6AAwFZABQsQBAL8PwjAHqRkQr4N+r/FYiSihpMAaS5cDWYAUAQCAiioaIBg6n+mkAKECWAioX60g0J8GWeOwYPPf9hS3xd08EqqWr4qHbwN69X0pvOua2bUkbePuHJ7HAZNeOLOVwXAArQf1AoqShA1EDwhSl9EQcIoqAAg0GUoH+bfyksVDcuSfBI/kUMUl9mVf3ndQ3v7URDo445NrjUORlAD7qYyQJiAagAJh0pwgQowvCtq8Lo6NZ3hKWXcHvR5NSyDl7cnj6bg/TpCb5Pajo79YaSXp8iWe8JtV4VK5jWTGC4xeXJOBAcbmULZ2K7jPT9GeLnfw4YpQFXa7hz+4VtYbdj6O1Lxlaaj8U1e0Lt8ADO+SNXfc5ItEK1rOChDhmmwL+VJ9IoamHXNsd+9El8gU8h7HXHn+EPo0j0NyqA/r9F27ZjMG71tTmNrHo3rQFK+c8YGojMKRMjZ6rO/lwiGtwZvz/wcescIMKNy+MI5EevhJekv6We8zDom8RZ2b3aduHQVrfPcjIoscTcsrAzIaaQflvUdaFE49oTE5Z/FW+36JTGZCKg03v2CZRmhNUB9VtJJX1IJVQZKfPbr8cnOohA0dpl6FFNVcGmQyjh/v4TtoQPuTjeP3wTx1cmrZWLRlgYh/ibK3/YzuH3MGi/zFLHsaH7GJzkk6k+bEc5zzKHeFPsJdlcg1MXH7fcfjyZfAUrGMJdIskxFNihuMB1aylurudL54ugoNJ7QhrBeMv9Buy0kMgTsMFmZoSmXJr4nL+OHmU/kjjAWH72cGOr/NOP4xll0T4u7+ttkQstkpHMXgyApb/3J0A5g2ter1hOS5vA3tSxjKdAzqOBmZ8S+VM4Ppa4xBXalrMSwpPhETup+wE2T5D0vhq1CY62g5YhtFrIcSE9s2WSpdCY/ri3qKvUZ4zPcn0oX2BVKgYm1oLxdVjxe8JGbZjyymJkMF83Vs6mOjKqsaI+38WOTQBwO7mt2inJVk5MLdymxxGrWOtJ9hCeZn37Mmg1aTWxaj09feCXRqAyWzSffM9Cs2tvSKzIXP8elHMPPmDCL0d0Q+Fe0CFZd/+Ru7qgUY3jEEw8Ebu1inEsaWL2SmKkqTJyJBkmZXZZ3Gt1EvW6q954yryg87Ogm2EaXDc1Z+SEcXCCjv5J7Bh+TXw3/C0Dl4GQfPje3I9gMaNgdIPhbEJrXf8wsICVL9KlWaOl47HPFVTNtJYhqzJv8YE+40+KsNtIB0uyd0v8cZywu0dUxUNiTLRiAPPMxyN9ZvKMfoNjVIxokT2VYCc+MHJ5wlLYZNYcQ0RLds3z9yqXnhxyEJ3EhIUNPsvDN/nKxkmdcBKBmazK4TrrNtPSOq9ozBUUpSBIMk7ShQS4jjEsJnN64uUMMfoT5vzvSc8tMPG8vYmIoFvzJQJW0UKfdQNstyp2BUsTvKqM20qcrvbRZTg6JJe1/DaTsFUxoeWGpFTY6Ovcbwnp2vr88QfBm0RLyTCLtYWVfp1Exu+5VOIEVwQc33Fa4icfjtabti7OqXkWS98iDA6P9/kKaOCuorHAbm+k6xhl25U4YhH/cfYzOqUBzIKuj5XhwfJ67lffe01l23WZo+Ws3taZOw8Wc+u0TWNGeabnzwE1YmVEtVcxTLCrv8xAM/HHafqcNnkQCmHj+4MPq/vfilBYuO86qWg7eGlm8IR9FckYbrFUJzv3FaFvktgDNoThmVv1aRY5Ql5l0jQ7j50ED272eR7oaXdl+/mbYAiLhxlT+vmct4znPjPO907o7vxjvgBPO/SD7qClkt7IRG0UsuBTsq7YUMpQUwdkbnHo+PFK9K8TAkix49q2nliZNhejiXSyca7noXGW5pC9eD3dLs8peis/bSiotH3hq0krX1i7u+dQHMpfYzel/0ETeMGkgYTOKUyW/V3D+8oVbgMdSxpUjGOrJ3e+uGvMqBrJ7DIycBs/pTyMfohdu8beiOlIGtKSWzXnTl429SnqhWar7zV1kTzW17fFMLnWpKBh17c3LTI4NBRzSXoPXnbsVDxVHUA6ZJcolySvm8JhMV8zmOC+BVxhyg7KxlRWKW8nKKQb8p8WXE+cS/ixWkyS7DQQ272raGJvsh+aFNvnVRr+pZDOC/0qxsZ3EEZ8KVcepoCWDGL1g1ZRsPgG6XJ2Zy3EXpsgDUxjK0GdSFi1S0jdmWqRVTyWSzk8qqJ4ILdWqH+eKgECgoxpibgI1+oNW0JhjqA5kwMl76pBGLZm8pMDo7qm2CPpAIOrA+zxaDdq6blPeJX8F7ELNaijy6slpkMfq/VdjAOKL57G0Nx9jI/Vq2jSgY4DNX8tN1bJymlhDTxuFG8z5Ejf+TtfPFUTH2/EeBlL8Lucyt26nicQSL6TWPzg82RcF+lMiKprJcc6jpHL9aBRrVr2cb7kL2hkkG3jc3N5oRWQtd2e2oFbhqS+nMIXF2iWMRD8yFh9mUuSS+W9sC/4OKpitGTpi8O3xgbsHbuzzAY5lilgtFd8TpyT+Eb1UNvvQ2O6ylrEAHApNj3aiu9N+vfLPm5sK7HPXMVfkprFQLGKKbkUnlsV9dN8GQ+CGBVwvR1nTVIuSbFYXGERfMFpu69mjD0pm5tcdjtSGgESNazpu3/6Ku+Lonz9N46lgpmQjNxKpc6ysj8LCtfEzwFw5Q9p0rZLR6bPrVAvKh5htQ44qhjw/M0Au1+gyhqZh6Lf6VleeM+hpJKnoArN9UEw903IBOqABWSScCZ/xK1qwYiN5a4Vmj1qPg7rxumeAwbunwO0SNP3ke/WeCv2fR+nnPZ1q4mT5fyXEjg/dap6fPjQ03v46aSzLkEKVXXivR0aoTWCV04fJrgrJzlsFlbo0ozVIO3NurZ1jfngscGFXLoRE90qXUgDX2/1soWPv80/lZ6kRuc8TlAWxvjLaPQqQf2NH1sbWdHF7cVrS0TPszbdX3pUXzQ199QDxtdolI/IjvIPzGl9s6LYNj9ls/jHpMv+pGhgPu1P1KlzZ857G68rLa1BlfGesPnWwAaVoiIFRsMr/QVqtT+1BQ2Dttrj2Rhb36fUsfoE6lZoGLMKERlorPLf+uTCFE3H6iMNi67UIgFQUuMuf3cGL4ODcd7GFMKL6PBNpkeH2dJ1i3bGVPMSIk9CzOCazvHY48/cb+wLE+L3QplL13qHtNbPsgrPPhbMm9EUR6dvR2W9F5bSzFSolbSzfi2DmNHeBqtApqgy4GbWeuHTXr/t/zgdWTcMe/TsbFd6X/tE9aDL1IHeJehEgH4i1ZReoTt++4WfzCjbkAKTtvzZKkVWJS925zazbPAobOEMXPlmStOe5wBVgU8inPj/gRChziAEWctQX7GDF04BCQ9IHKMSrC9sTnB3Gkl9n8r/3NWSp8NJvSHawJDcOcRKsy7tUROXkIUeIBb1NxfcANECgFPWL+P4rXi+6MKlUgPwwKtpcixX6omC4M6wpDmkbPO9xzO0R0lpzETR1ArNYqC1bMT3Iv6nwSd8Rsk/Pr86vPFZt2SIiTWiHQegMhrLhU4opxZOBEuX2xztR2eO3Uviau2si2Z/0yH/+tFOuqx9sQ9vU5jemBnTY2f7em+R7Okmq/DwwjaHffUulKRbdt7drNgGt7jqVF9slX+zUBG0plSfOXCMuHImLUOBXE6Ja6Rd9VQY3ri6kycZwBjTHX1PS1tnIB778ZWg/qRm3bh5n5AO/YRqffFInNaOusm9UoU+J1tCRITJZV1x+mPp8jlTCEFewu53hdqi+a5uqXVW6OLQfqqLpwWE42Z4wDmgacpo4xe7nJ2X5ZfY7bVv1o7f59AJCAQ3fvsa1lbQkO2Fup68RmaAbQIERgVIKRFRi397KQd1L89ex9Mc4J8JI0x0Spx5l30heKg2bfCNPKMStGO2S+zyZ06c6cnDYIJrEFNcGknOrfaL+G4kYrgL1SPjwwaFX9JuB1VeHFPp9+ZaruGW4HQPmOAy9OyRHM6k4aqHJvirL1dVdOXiW9H0xDwvW3uv0cIXp4SU/ld1N2cDYk8HT67jkq20braFCdCnP7A0K9wDF87nn8jMvtemnR2eaoMcHH5jW5uk7EMmX7W3cQetQKK9UXGemhmhb5ydyU+Wyn6q1O3aUTAKVf/6VsCkXjFcZJmzLOoNw+tupwgLmOEZNxFTkJ1IntpNrYa30wZdm+pJsgItcSnrLddVpgtHItalb1i3jRh9HY9azteHe8UVT121Q9rWP0+p+5GMVyB9f6r0Er82csJGTDk68McR65d/vPBKaS6/oUzD9X5Vce1LMDHmPt/YRECveGuUaVL/dtgPzWL9y5aB5EvqPVqdRq1Py1a4y4NKfSugu4tegfuENzkN/B/6T94raeeouJpWj0RjbUqHgZjFiJHVnbcflPnf9ZQzIseN2t26gRsAfYbKW7aYvA8QlzvfN6+/dlR6FNbjNvY5ExH6qTAd1tgSumH/CrxeVoOaGJ2kDOmczEDl4pEem72GH3JQVa5FQwm384d8e91ShU66avarjFECggUpcqk3UcWxJkExMV2aLrrlQt26TAK7zC6Xb5MBeRBAntwXLVrK5HbGll2ZJCIfdt8nn2nyOWA67Xbv3jtFISyd6bGRPNroAV43tYy4kV5UfJSiN9Ik4VzHsHzyMdaHN7/+JGztwBwaqjRe7bQmokNVkhXM+mLCllC3k21mmTrtSCv0LwwnZmNQrDr7mUd85ILcLYKIXcAB7BjEN4KQ2Sw0fcte/aNvRPIugNgfv7UbGbDn+40ZjX7m7Q4ZN1XQZVVcw6youlxSK7Mi79cOCNpundK8c6Jcur2wh5naKPauDeVFuLg7LcRiUKut3MVTpkfFsx3mG3cgFZeaymCd82l1Dw2wBuJK8co6F19P6jngcODhr9JQHXhwSNGB7Wl+vdVyc6BEmgJp8GDs7jOz7cxS57FbyyYhsqeMlnVlsSUMjcqlCOdTYR8RwvFXKnRv/QhPjYaYXq/92iE0g/1Ex76nU/91d3d2FbG20Og1JDhxwNo5XTbIOG2KaP/nD+FZjO70nvNQPdXEhvPRUI42nUfI073D31sV3biK7yIeIsyXGcnt2JrWuwoj8221CRZ58OJ/LZEXkkCaSsOe1jwYK8qNLd7++qffqPqDcgEOT5GskHwfRatGrjQoklJ2hlOQ27emmWrDngUOlM4VRyP8DEArtwxnVWrRgOcDP6FcDDMqK5hTAaqsRudoNRqylyf/qT4X93t1oDhdcquBYoSysYK3AwftmdCprE2QI11cRHIZ2+sRbVyT2KUhL1iaaqOeVwBTCNC9/ZoxuB6IZP3/AoeHCTA5/m9iAabz8f8Bl7U9dHoQAAA= - H4sIAAAAAAAEAJWXZ1DT29bG/0noRQztSKjSQYo0gUBE6QEpSYAICAhCICBNEOlSpYP0DtIJECH0okgVqdI7KohSVDooRbm858x5Z+7Hu/aa2R+emfV7Zs/68Ozz2fNPwGUdTaQmAAIBAOjiAOcLQBhATUFJRUlBTUVJRUNDTUvPykBPR0fPzsxymZWHk5eXh5Obm19YVoxfUEqIm1scIS4lJw+Hw/muqairKKjJKsLlQTQ0NPR09DAGBpi8ALeA/P9c5x0AlArwB/whICgAhoIgUNB5D8Bx4ZEc9H9W/79AZOQUlBAw1YWqexkAgckgYGpyWiraf0QwBCAjh1IwXpVCUTJJ37Z2D04sYOYldY6xsPLJoTFGNo9qu1TVPJKSx5d3/uIXEJRV9wwJLdy9GMZ+QQFD/gsFJgNBLmAXouKFJxAERA6hpiSDAP+iyKBS5IxXUe4XHAomZl4ZdAFpmYVfVjU4cUwN07nDyvfofB6gg1zMhUKggAqweB8M8IABe0zFO7TdUK+OMwHXjoluOIlQBulnLF9tKdO96xWnhMLKVBgwz23PdNziUP9J1cCU2eJrh2i/b37N8IdoUxhoYrEq8GtFz8OiAsbhhd7rYFHzMCTKazMJY3FpsNhXHPC50sSK7jv6wOFiTT2/Gj3wdLPy5D51hSDihXGOLpchQbEEv7oftTIV0PEd8QyoC7n0DvSPr4tWvUWWBLAjg4Qf5zjLqdxotaS3lnENTe3/whDilD18rFVmfw/Evo3L5llrzXhbWBG2+9cvY8sl4CGkWZMdaZcUayffxpImk7/Mi0YKRDNHgzWDwmaf5NtiU4dMcvQb6GPM/HE/A8Ve6j2o0oBXRsWLuej9bi8f38I9FvkOp71HCq7GTuq8HvVoufkuotgg0/mhn6vHyjmQXKmQf3kwu9TH8fSWL5Hc/aatEImYv9qnqYf5pVupeznzK+Z6cGPfwo84UmvAeoG5tdbJI6dqxscsykKx7GIGuo2TXeEJ2Q/O/CRZr5pIj36+dxfb9Flh52tzLrsT4/1RzpKbTszuf4ynLLS5UeZaJiOR8xiJZg/oBIm5IPD2RC4MPvAWC1NKQfjxUEcCQVRAEFSdLEkb8MiAyFcZ43Z7TlOyZCpx0eO1Q8P8z3FU8BKhVV/fxuCuhkh8xk1tjAPxRTt5neAMor15Cl1RNvnazhq9sbPMhcMjVBDYDEyyPrFLby9EmzNR9bGAfM9hWqMn8XOk5OT6EmKy3sJM7LmETjyJuNpGP/QpyTEj6LmF2yG/mXTRUMzLc0AUpl9SjEiN14Z9C7ARW9JrrONbDBVXLimZcCavcmZDQitFtK4wOuYmCmhfRYLX/GGdLCjdUHXz2AWUmyYpZUiHTVzBnEN/nEOXaQCzl01tPBBheQ7cMS1xm6HmYP+RCMvM5WL8snQgvn9QPMy6y3U5Q5dpnS0a9c7TxZD99p/HMYSyXi6cWwr/Z4FnMZbQniEbERZJISi8s19OjEisLPI1kIpNFPp0sVw0DFRyPlFzb91KBHoFjou/5jymNMk6lmenec3pRmbCV6mzDLvmmXVf27tMhESaI/52ryDq9a5YkOqeGEsdm1T/3sbda2iT3cv1MV1EtaDW4j2LFwYRuaKoMCPemsTri4uvcpp1bqN0Hg/cOOq8vjEYY1M8VSEmgJ78Vu//yWNTy9P1wEeOMGDu+CEna8mHUuTwp+IjGoRLvi2C1b9f/LVLfSjsjpSS4Gft3BSxCCS1YnX5X0uwTUM3U7tBBFF/syw7+n4K4d5zvYZxWU6T8cnvmWWXQ8I5+nmnlcdyai/lmPePKnct6FiAP5mPRAnic/jzDAd80kJUs5T0G+WDw18d8tcECgQGw1PE1fub/Q+SC+EBpPY4i76IgoH5GzQrSmM/qhMidiwq171SlSRjynkqxcMmRODKc/MMxVzl61wapc0ymiUGg1Mie/t7TiM93dYOCMqMg8grnB10lXKseGZCRWx/HXo4nWXCxWXpOYVTuHe/2cM/QhWz7/XLWzyGA0LLGUAQKX0DWgy80qDFqLxNeiW6jinttCgTXXpW6+ekZOeMxJj9PKQ2oTkyb5RztBqIVhoaLGjBppQwqjkqtRQi3eUmy3+y1bg9FRvtrp3LRirfnV6CTplxqxDeuhJwe16BeM6xJEGnbjC786a0YUoMsbts7o+UybeaJ17g08F1VIlx6vIUM4OVXIqCmWE72nWCTU5wOtRONqlE8GjpSqeQTNKcCNkitFnzjogtblPcm5dJ4xwIje7ITPeNgatOpH8Kadff6v6kbtmUZ7LxpXG4IjbVe/3w26HQL+UsC9gbw659ml8sljidtey9cWUGfzMFXBxgfyauJz7naEyfXHU2iun2c5j+WIVZAkx+qecwGhHMzIWERYDgtnOAMhbbnMYXb499MLqfzr6tnfkN2VoYivBkNRUSt20K2FFfI6wUfyHscxcZ7kRpYfSs7CWeLYRgksIlbpbHVyhDJvODQyWXIz8Exit2Wah4iS4dQT/uBTz9KBFOLThAobhW7cs52IpnlK1LSi2/LhWEfkkHVsNq9NC9O0TmGN8gSCrkKjQcL4nAuWnfvtP1Qqa4lItOlhhkyd6z3H2eW7hyQ86obkNC2KNRL0UpKr5gWc+URmc0lWeJ/x60FRja5R3aprnTSDRPStR79kW4PB+ctw5O7iJ7z22FVszWe+W8GS0vOa19cMqYJ6zj//Fd8H2PXtyTjQTVKj4KtxvlOeMuZb7OjBF4mIsCKnlH/hV/Tr0J5RR0+qb/nQ42KzoVtvGnmtvL0ekBKb7cQs0z6aoRzNK+P8Zuj8skXeLRW05SYNyRCu1qQAanv5jqrL4bxafeLKP96FCfgSl/xPBBD+2VjXGfEPuI9pAyD4WXuIjgr1OI0PX9S+xEdFx47ia4r0ezq0D7sWmzwcvkBio8X2XMHdDt3jK5QIeDyLVFP7WZ7wldElUJ6XNzLHOzzyIcouooRYh3DTcgPeobR2OFuzYfXDtOILQxqNlTXvjaSL8D5Npy6Gcdg/WlMyn9/doErdWPKwKjhfOKffXujGNJErxQtn5ij3Y/oYrK60o/tPC4tVB7TEa9m/IGFVq/6NobH9RY05F9/PzHmPSNPY2FkAb7jvqtkE2/XMO4PtlK4/dJOwUo5MSAQJab53HLUzqQ5lv6tG/v8YXhZ3c8trAvfYfjxOY/J9vatkzUXqtrMlBrn32mC41asTVNPoooVt/wGiv9o/I4X+1ptwoT6UjdiyGbsYbCtJvGRJS5CaJPqG9cRz+fM+TIbYxuMnwU3flhVm5jlChi5K359Av1cdMbm2pb1D6LIlf1DyfJraveu/sY7hqrWi6bMl4Vp9lzwJ3L6IRRovGjjs9ApiGki8umcEvqiJiSxzwS5hKHT4cpJxdE8d1Jo4h3rn+ogc8i5z8WpKSB4/BO2/wnnwtu22QpW06mHS6eA66zb3ZwM0zBBJ54c4330yjsS3lmmj7q2GOKa4VTVWArZ3QlVlsAeTqwFSR6ZdNyW0Y2sYAN+cf3lU3JpLcI5+8IbOzGzRJ8HB3KvqIYDparXUWNeCCxl75xX8mVmJe1kmwzKGi3fuArw22r5XAcJQncyEmycWfjvmW4EcTmt1T6mxMxCj79+PdNxqVQqF0XVvFStkt1QnSZYkF5Appul+3gd/dQGlqhZeTOVTbpnlHUdQDlx2fBVjEvBervER2pDUzjKqM0i1M8hBYtd1HSGQEV/m/EO6K8OtbgIM67VWJc4Kol5IDr69WZ9T3t0nHbYnOA1zRMFjghQKbYeG7tpGoaP6AZooBmOaJU7WLoPYQcOPGinDUU7m/lLbsJaCOaaBCle6TRszMzMbMZr8Mi1BP/tgFfw9JpNMmOf/awT/CeArdaUZSA8HqfyBxskMBhfw503nzCk5F6c172g9jCsDf7NFSxvHZtM/JHvrjgi78DWHBf6Gnu3XNAc3tJKqKgMaIhwcj63apQuRb9ZoA+IsX7ia0dbJMXQ/nO/VBS7K6yUeNrJyVK3If1Fmf6DfvEr41TXSVaw0S7vZM0X47qdg4f0TG2L/j8acs1tj6ewMY3P6RGlYNrIkFUr0CxWHwftU2Z3XcN6ULd2r5Myr3EWQP3y2SCPGW2bsM1EsmDXKHMEoRAT9/XwfV/LVeonJ0DZKp0hghflq/dRPFaT7Q2gpSys6dZrLzKoZh8sIiPxNe+S09/pN0YbJ0ZbrWt8fGOf4L0WUbpQcbPE82eNsI5QMJuq01Y8XLvep+eA4Ayn82DusXdcwCcsBJR2q6JTdr9OfJ0/JKwSn6igRHp93pf6WCi3GqvZEDOvb4rJd272XoJBNF8i8+X9vzjEvccl5wEpfJ65E5i22onGiIWrSdTmmfx9eGQeoeMmLby4Z0933FJZe1hRvEh8kLCoi4tWLo72aAgjSmzN5tfveAvoTBsSyWN7BLyFb8ZLs8mWGsla0otx9ndPk9J9lMB56T/h71NrHEuMjCPakxBoXvEsNxnDeMYu9l8nP1D71j4O5d8VKquLXDr8mw1w6CNjlIt002bloetN+pJJLXjOcD2zktHTfdNOXzN4me4E6lnX9iVggKD/wJNhVvfT7ebyCSW8O09DfNwWlA4GR09OuVzfttq5PTKoGX/JHHDCINg2/o3k98nt0vH5dHVhDRU3ocK9/avCGYdboyGEoyXeftEh14zGjuZdXonhD1xW+SrVA0pfWUAavAq3i4L76LV+GOq1PmHw7fN0BVqbIq2zTReNbVaMxiBNnt4xmzFrfFP/bN11Q/oRASiiscjkDbulZk+xaDMurYSoZxbbMrr5ctzYwbVpN7y/JAMfeIr+dDNeZ5LTNKME1ZUA6FO8CV48ZGGWSMpn6Uz95T+jeWSsPFh0a66xx4G/VPUgaAiwuUWzVSAhhVNzf8w183r2Tm+SefCeoOTVL6eV0cFogkDiOGvqqoKdE1WY96iXVVZRLuSPmR37fILrI+Uf811u56QJGbip9hmM6WEHGzo+GTpo+WKtIm4SL52h1wqKgI66eK10pCSZ5WmG/HhcPdSEtO7pQ8zbXm/Te2TtiygD7q8YS71+8YXCTcjCTk0FNKivyXhwsn+ZCsqld6UWoeRMYjxuRjACtzi+ucleChSRR/molGPUtlwX69YTipkx5OaDzrYGpxCqfsFCGeSiMzqgmHOXGqrpnaAoI8wKlLqNSF4PAoLdHuR9h4dPvEZ8NbT17QF13anz+XyXhV1Syuv09lbSajLtiJ8h7nP9H85pT7qQcDSW83Mcb+fGp+RSWKyZqLSOG3+NIsypyXyIYgBchHGZ+1a4dgsmhU7l5WR4BavLYP7cQSBAtXHQqhtvcYzU3HdrtmKtlTt1J/SFjZnnp9qiaPx2IAifj1a2k80eqCeYUnZQ7UNGdgXFZQS08ZGkHgNCLj8b7MGKRNE0DpGz9FeITxUpt+5n61NY+XUE9EjUyXPU7nGHgCalaH3+It2Udw467FfLy5VufmFzfFEX6NtGiHonvZ0MI0ztwYogpiP2fBGl3AwHcqgIE4z4O7fXyMqyPncfwD6Q4oeZhAAAA== - - - - - - TOEWAvT9GlIjOkSMv9F+aw== - Root - - 48 - - 2016-08-27T14:41:45Z - 2016-08-27T14:41:45Z - 2016-08-27T14:41:45Z - 2016-08-27T14:41:45Z - False - 0 - 2016-08-27T14:41:45Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - GPv+n0B4upj0sZd+nuuiBQ== - 0 - - - - - - 2016-08-27T14:42:09Z - 2016-08-27T14:43:51Z - 2016-09-11T11:11:59Z - 2016-08-27T14:42:09Z - False - 1 - 2016-08-27T14:43:51Z - - - Notes - - - - Password - - - - Title - Test attachment - - - URL - - - - UserName - - - - letter J.jpeg - - - - True - 0 - - - - - mwq3nFNpPrvjXlsWy/6FTQ== - 0 - - - - - - 2016-08-27T15:03:52Z - 2016-08-27T15:04:44Z - 2016-08-27T15:04:44Z - 2016-08-27T15:03:52Z - False - 0 - 2016-08-27T15:04:44Z - - - Notes - - - - Password - - - - Title - Test 2 Attachments - - - URL - - - - UserName - - - - letter J.jpeg - - - - letter L.jpeg - - - - True - 0 - - - - - - + + + + KeePass + + 2016-08-27T14:41:45Z + + 2016-08-27T14:41:45Z + + 2016-08-27T14:41:45Z + 365 + + 2016-08-27T14:41:58Z + -1 + -1 + + False + False + True + False + False + + True + AAAAAAAAAAAAAAAAAAAAAA== + 2016-08-27T14:41:45Z + AAAAAAAAAAAAAAAAAAAAAA== + 2016-08-27T14:41:45Z + 10 + 6291456 + AAAAAAAAAAAAAAAAAAAAAA== + AAAAAAAAAAAAAAAAAAAAAA== + + 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 + + + + + + TOEWAvT9GlIjOkSMv9F+aw== + Root + + 48 + + 2016-08-27T14:41:45Z + 2016-08-27T14:41:45Z + 2016-08-27T14:41:45Z + 2016-08-27T14:41:45Z + False + 0 + 2016-08-27T14:41:45Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + GPv+n0B4upj0sZd+nuuiBQ== + 0 + + + + + + 2016-08-27T14:42:09Z + 2016-08-27T14:43:51Z + 2016-09-11T11:11:59Z + 2016-08-27T14:42:09Z + False + 1 + 2016-08-27T14:43:51Z + + + Notes + + + + Password + + + + Title + Test attachment + + + URL + + + + UserName + + + + letter J.jpeg + + + + True + 0 + + + + + mwq3nFNpPrvjXlsWy/6FTQ== + 0 + + + + + + 2016-08-27T15:03:52Z + 2016-08-27T15:04:44Z + 2016-08-27T15:04:44Z + 2016-08-27T15:03:52Z + False + 0 + 2016-08-27T15:04:44Z + + + Notes + + + + Password + + + + Title + Test 2 Attachments + + + URL + + + + UserName + + + + letter J.jpeg + + + + letter L.jpeg + + + + True + 0 + + + + + + \ No newline at end of file diff --git a/test/src/main/resources/EmptyPassword.kdbx b/test/src/test/resources/EmptyPassword.kdbx similarity index 100% rename from test/src/main/resources/EmptyPassword.kdbx rename to test/src/test/resources/EmptyPassword.kdbx diff --git a/test/src/main/resources/EmptyPasswordWithKey.kdbx b/test/src/test/resources/EmptyPasswordWithKey.kdbx similarity index 100% rename from test/src/main/resources/EmptyPasswordWithKey.kdbx rename to test/src/test/resources/EmptyPasswordWithKey.kdbx diff --git a/test/src/main/resources/EmptyPasswordWithKey.key b/test/src/test/resources/EmptyPasswordWithKey.key similarity index 100% rename from test/src/main/resources/EmptyPasswordWithKey.key rename to test/src/test/resources/EmptyPasswordWithKey.key diff --git a/test/src/main/resources/ExampleDatabase.kdbx b/test/src/test/resources/ExampleDatabase.kdbx similarity index 100% rename from test/src/main/resources/ExampleDatabase.kdbx rename to test/src/test/resources/ExampleDatabase.kdbx diff --git a/test/src/main/resources/ExampleDatabase.xml b/test/src/test/resources/ExampleDatabase.xml similarity index 99% rename from test/src/main/resources/ExampleDatabase.xml rename to test/src/test/resources/ExampleDatabase.xml index c7367899..6535b834 100755 --- a/test/src/main/resources/ExampleDatabase.xml +++ b/test/src/test/resources/ExampleDatabase.xml @@ -1,583 +1,583 @@ - - - - KeePass - - 2016-09-10T16:29:17Z - - 2016-09-10T16:29:17Z - - 2016-09-10T16:29:17Z - 365 - #FF0000 - 2016-09-10T16:29:17Z - -1 - -1 - - False - False - True - False - False - - - - kJh5KNGNaU6SZIsfsAwx7g== - iVBORw0KGgoAAAANSUhEUgAAAHkAAAB5CAYAAAAd+o5JAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAG3VSURBVHhe7f0FeFXX9j2ApkXi7o5Di5dSpe7u7u4Cxb1Fi7u7SwJBgrsU1zjxkIS44dB2vDFW2NzTXArce/v7t+97b33fzDk5ss/ea6w555hzzbW2Hf4B7ddff8XFixcv/fev9vvvv5tHvXf69OnL/9t+/ty5c+ZR7fz58+Zzet9qFy5cMJ/97bffLr3yr6bjWces2nQMHU9Nn9ExrM/qWNbxdHzbps9Ufe3vbv8IkK2mDjp79qwBygJR/9sCYQGsxysBZzWBX1JSgrKyMhQVFSE3NxcZGRlIT09HZmYmcnJykJeXh9LSUpw8eRJnzpy5LBa4VZveO3Xq1OVzs35f51hRUXH5davptX9C+9tBVgdJrjT6bTXWaraAW1ossHbu3In58+dj2LBh6NChAz744AO8+uqruPfee3HXXXehVatWaNq0KRo3bmweW7ZsiVtvvRWPPfaY+dxXX32FH3/8EZMmTcLq1asRFxeHgoIC5Ofnm99Q02/bDoArnbM1EP5J7R+jyeowaZRlbqUltgDrf3WgtDMpKQlLly7FoEGDcP/996NRo0bw9fWFn58fwsLC0KBBAwPqAw88gAcffBAPP/ywAfPJJ5808vjjj+ORRx7BQw89hNtvvx2tW7c2oOs4+n5AQICRwMBAPPvss+jevTtWrFhhfleWwbbpfDXYdK5VLcufWYT/1+1vB7m8vPxPza4Fsjp2//79mDhxIt555x00a9YMtWvXNqDccccdBigB9sYbb+Dzzz/Hd999hy5duqBfv34YMmQIRo4ciQkTJmDatGmYPn06pkyZgvHjx2PMmDEYPXq0+cxPP/1kLMBnn31mNFsDQ8eW6PdkAfQ7L774Inr27ImNGzcaU29rWWxbVa3/O9s/RpMtX2wBLj8aExODoUOH4umnn4anpyc8PDyMhr7wwgt4//338emnn6Jz587o37+/AXDlypXYsmULtm/fjj179uDw4cPGD8sHZ2Vl4fjx48jOzjaPEr2WmJiIo0ePYt++ffjll1+wefNmLFu2zBxv+PDhRotl+mUBZDWk8fXr10eLFi2MddBgkavQuf9T298OssiJrTbIv0ZHR+PLL7802iPzKW2Slspvtm3bFr169TIauWnTJuzatQuxsbEGMH23uLjY+ESZfw0YDR7LT8odSPRcr1lcQM/1uiyG3IGOk5aWhvj4eCMaOPL3o0aNMoPqtddeM4DrvIKDg3H33Xfjm2++wdy5c5GammquQ9dUWFhonv/d7R+jydI4mVX5SvnU5s2bG9Ikje3UqZPp4MWLFxuNExnS4JC2C1SLAMmXyz/KTApIdbIepWUWqHrfEgtkib5jvaZHvabjCXj9hth5cnIyDhw4gFWrVhlTL8DfeustY2mk2XXr1jXnr3MVi/+ntL8MZIuAWE0ddKWm19WBlvb+lluG6SPG4v42dyO8Xhga394UDz37KD784kP07N0Dk1ZNx4a9a5GRGoszx3OAnBKgmL6O0cnFkzwAieyv5EKlhReQkV2MQ6kZ2BR7BEsPkG3v3ohVa6IRvXolVkQvx7IVUYhavtQ8Ll+5zLwWtXEFojevxpZtm3FwzwEci0lGfnoByvPP4UwJyd9Jnqc8COXs6XMoLi3BiaI8JKQlYMe+7Ri+bDa6DO2LLz7/FG8/8Qza1G+MRiFhePb5ZzBw7DDjFqymgaRB9Gd+3LbpM9fzuetp/zPI0pCqTSen1yW2saItW5Z5XbRoER6+9360at4Ct7ZqiZdefB5dOnfExAnjsGndeqTEJ6IsLg2njhfizCn6bH6vnHLit4s4lHkca3btxfytKzFr1SJMXzgN86aPx5JJoxE9agjWDOyLVb17Ylnb7xD1/bdY8u3XiPzmK0R8/aURPddrm7p2wtrunRDduzsiB/bGvDGDMHP2WEyLmoEpa+di3s6N2Bx7CCkZmSjOzseFYl4vx/KF87+jqPwUfsuvQOKhWCyKikL/caPxZbcOeOG1V3D/7XfhroZNDYGTqa9quqUQFv/QoxVR/FXA2rb/GWTLVKpZps9qtidsabneF1H54osvEB4ejkat2BFPPooffvgBMydPxdY1G5FwMAYF2Xn49QyPVcQvnaWm8umuE9lYsG8HZkUvxdLFi7BhwSJsHNIDyzt/iblvPY/ZD7bBfA6YiNC6WOjojVl2NbD8Rlcsu8EFUXbOWGrndFn0v2SWnR1m2t2AGfZOmBEUjFm3NMPcZx/Ggk9fRUT7j7Ct3yBsnTwJK5ctwMLNy7H8wA4cod8ty6b5yOKJFdNicfQVl53G4Yx0rN67G9MWzMcP37bDk3c/aPx2SEiIIW+HDh36g7WTK7la+8dosppORloqc2QLskamLeuU3xVbvueee9CkSRMT9rT/sTNGTRiDdevWITk+BWWF5Th75iLKadbyT5UhtaAM24/EYv6yKMycOgmRw4diZcdOWPjMi5h4cytE+fhjsYsHFtRwwKLqNRHt4ISNrh7Y5uaJ7Xx9uTdB9nL5U9nn74e9Pt7Y6eqOTfbOWFPNASuqO2IpjxfJ/5c7BWJFs1ZY/NbLmPZjW0ybOhiRUXOxde1q7F+7Eb8cS0RWUTFNOQc4tfxkbimy03OwedceTFq00Azm5557DvXq1TM8Y+DAgX/w1+o7ifrK0myr/VUh2F8CsvyM1XTCFtDWo4AWE1bIo9hWF6t4VOHHspWR2MvRn51zAqepued/+x3FZ8oRl5WIjQc3YfysUZg9bigiu3fG0ldew4LGzTHLwxsLnKiJrm5Y5e2DlT4+WObtgcVerpjrQY30sMcMLwfM8HY0IEtW+LhdUWYFuWBekCsiA90R7eeFdb7e2MhjbvbywSZPbywJcMQyj5pYcuONmG1XDdO8AzDt0Qcx76d2dDcjELVtJTbv2YT4mIMoJHjnC0pxtuQkcnILEJOchg0bNmDAgAH4+uuvDZFUKPj222+bsM3WlQlggWrLZf5Rmmw74mSCbE2S3lOq8LbbbkNoaKiJNxXXRkREYP369agoyMOFM6dx4eJvKCytYDh0FJtXLcWSyUMx5+fOiP78Xcy67y5M8PRlJ9fA6uoO2OTmRmAdMN+HHe9cExGerlgd4ItNQYHYFhCIXf5BOBgYhriwusjwCDWS6RmGLK9wHPeuZR4lem2nly9+8fbDbloEyTb+v9bdA1EuzljoZI/pvnaI8rbDTg9HHHRywy4Hd6yg1k/298KQWj6Y/+FbiOjeDuvnjsee/etxNOUI0jPTcOYECSItulKjitfll3XdCr9q1aplwq5x48b9W0JFimFp9D8KZItQ6eSstJ9OUD5I8a4yRTfffDM+/PBDE98qW6QUodF0AlxCcxd/LJnavgWLx0/APBKliJeewypqzBya4IiaTljn6klA/LDFwwPRbg400w6IDvfEFk8X7PLxwCFfPyT6BiLdJwg5XsEo9AxHqWctHCbwR4KDEBMagtiwUMSFh5lHiV4rdAtFkXsYirxqocAnDMf9QpHkF4RD/v7Y6+eLze7uWO/ljpW+FH/+trcb1jq7YFs1J+yhb5/jwMFwUwNEvvEcFvftgKUkbds2r8HxY6n4rficGfQK9ZSQ2bp1qwm9vv/+ezzxxBNm0CvhsmPHjn9TDPXpn0Uo/2n7n0G2zIz13BqFBw8exJtvvmlGrcxU+/btERkZacyUwgqNYH0vjyx79949mEfNnjBqDCZ+3x5TH3gUczwDEUFSdNg7EJuc3bDU0RFLvN2xPNAHa/x8sMXbF7tpOg8He1FrPbA/yAMHQ7xwpFYAjtYOxKHaQTgQHoA0anZ6YBAyg0NwPJQgXpKsEGo3XztaOxgxtYKp9UGIDfZDLI9/JMAbBwLcsTfAjdIAv/jUooaHYCsH0CbfAKz19sZqLw+6CjfstLdHdI0amO7ohPENGmH2e+8iavIELFq7EhM2RhtwdZ1K3+q5MmwLFiwwg//ll182WTwleY4cOWL6TU0AS/6farJCIQvIqk2ZIjXb95UatJL+777yKnoN6YOl+9chtTANv5YU4/f8MoZQBdianokJkfMYJ/+Mhe+9jajGTbDc0QUrb6hBk+mFTWG1sIx+comHK9bSV+4ICCChcscemtMEv0Ak+AQg2y8AJwKDkR8cisKgUJzg6yeo1Xl+/iggwMl8LTOsDrJD6+B4QBgy/UKQGRSOjJBaSAkOQzq1SQMhg+DlUsz3JYGBKKwdjhgOqLgAPwLvhz30+9s93bHNl6TO3xvbOCBE1la7uiLa1xcr3b2xsIYLZgQEIeKNp7B1fE/0WD8HsceOAbmnkZ9ZgtTcYmRl5mLL6jUYNLjf5eyZsnua/bKaNftlWUnbJkWy5UHXatcE2dJMNY0smRUBajvKpJVqMi9LlizBnXfeafK7Ms8D+vfFru3bUFSYj6wTx5GSk4Hs4jwcOrgfcydOxux2HTD5+ZcxscFNmEvw1tHfbiXhWcnHeQ6OiHIm8XF0wGZvTxwIDsZhAh1HU5rCcCeFnZlJU5zB59LWJIKbQEkiaJm1aiOnYUPkNmmKrAYNkU5Q0wPon/1DzWNqYCiSA6jdIfTPHEy59eqj8ObGyCUxTKUZj6O2xvB8YgheXCB/M4QmPzgQB/h8j78vtvt4kpi5YpuHG9bQwqwi6GtI1pbXcMZssvPpdcIw59G7ENNnBCaROC7avQHl5BwovYjsklIczUrH4Z17jF+2Jl00Y6ZsmmW65c+tJpJm5SSshNL1tmuCbDtiruYjdBJRUVFo06aNmUSQqdZ0YHxyAnKSUnEhu5zu9zccYaw7b9ViTO3dFZEffYx5dZpiQUBtEwZF0yRvcnEynbeKGhPp7oY97LjdBP2gH8Fj5x9jzJnEjk4S0IH+SAzna9S4Y/VqI4mgHmNnpZMD5D74EAoffwInH34AWc2aIp7mWaCmB4cjzT8EyT7Ucv9gZNEPZ4XWQ3aLlih48D7kPXI/su68DanUrNT6DZEYFo4Emvc4anx8WJjx4/Lze3x9sNPTg4B7Y4OvK9b4u2MjtX0zLdAaAq3wK4LnPs+zHra/+zGiRg3C3FXzEHMiDadPnUdxQTnyKk4Z3iJS9t577+GWW24xBHX27NmmT6VgtlObVbOKtgp4tXZNkK0RowPamg5ps35QGi2TrRkg+ReZHoVHAlwXkFuYY7JVZQWnsHv3UcybMx+zevfE7GefwszatRiTMjZ1cjEEZytDoM1eztjs44qtZMvbqblJAcHGNMeR+UpDpWWJBDyB5jurVhiSm92MtNYtcPzeu3Di8UeQ/9wzyKOvy3v9dZx4/U1cfPEZZLRqjkMcGAk065mhtS+DLI3OIUHLCK2PtFtvxfGnHkXOq88i94WnzACpeOQJ5N7SGqk3NUFCrTqIDQ1HLAdZDK2GJJaaHRMSgB2+HnQnLtgQ6IUtPO8tXt6MADywhWRxAR/n2N2ANc3vxMohP2HaihnYcuAXFJB9Z+YWGSuofLgmWj755BMDsiY9lA1Us80zVAX4z1xo1XbdxEtarAMLVGm3TIdlsgWwJujlV0QilNjQdJ+YZXFxIdKKC7Dp6GHMnDAZU9/9DItvfxBR1Kj5jjWx2c8TaxmerHCpjlXu/D/AA7uC/RkG+WOHpw8OuvvQL7JDCXAKTXDuzTfxMYxaHYyilk0I7MPIe/IxFD7/NIpfeRkFBDf39deQ8fJLSHnxRZx77kmktmiCA/5+iJd5J8gZDK8EdFZwLZzwprkOr490coiMZx5F5uvPI4vMPvdpHu/xZ1Hy8GPIvfNupLW4Bak3N0VSnXqVAGug0W8n+gfggI8Xdni5YR3Z94pghl8BjM096KvdnLG1Rk2spEmfZHcjFgfWw+7OXbAiegGWbl+HE/FpxiQLSBFVAa0YWskiKYsURU2Kps9Z5trC4HrbNUHWAW3tv07I9gcU6yp1pxMTwJqW08mKTaqVFBRi9Zb1GDt+CGZ//w1W3P4AIu1cKXZYx9BkiZ8zovxcsMzHGSsYDq10d8Y6WoTt9K17qHmHazHWbdEK2bfehiK6gjMPP4zM5jS/9eqg9J47kf/SizjxwvPIee5Z5D3/HPL5vODFF8yj/i++926kNGqIoxwkx2ieRbgErshXFglZuocf0jjgMkkS8554BAWvvFB5vKdoER57GoXPPIv8R59A/kOPoPj+B5FNkyrTfdjLCzHU2L0enjjo4YVD3j74JdAXq0O9sTTUA0v9eS2eToZ9r/Ek4CH+WOjgihkONPPvfIxjK5dhzablSElJMSZZ/bV7924zzdq1a1fDa1SetHbt2st9aYF7vWbaatelybYmw/a5JtkV1Gtq8JVXXjGT7crwKC5UmKQ05srlq7Fi4iTM/+A9TKlXF4ucnLBNzJmdozTkHOeaWOHrhW30fb+QBG2n1m4LDMGBmxoh4Q6ayvvuR9nzL6L4mWdQ+vQzOEkwE25pgUMNG6D8iceR+fxLyHj2BWQSlJwnnkQRpfSxR1H60AMEpQ2yGt2E9Lr1kBpUqb2ZZNgCWWCLjB0jWMfIxNMb1EM+fXkRyU/Bo4+h4MmnUfTMC8h65jkc53OBXUHtzr+7Df1/XcTTL6cz5t5J93GEZj/Zmz7bJ4RWKBhbwgKxIzgAexnmLaPr2ckQcJOTM5bQnC/x9ccCOw7uux/EnogpJhJRP2k688SJE6ZPlR0UGZNllEYrr2A1K5pRszXfV2v/EcgaSdZzjUCdiHKySleKLKgaQycq3y0/I62eNHkGRr/2AcOKOvS9ztjgUZOEqhoW05ytY6evpb9d78VO8SK5Caf5bXUfCh5+HFmPP4qEx+5DwiOPIp8DKOPJp5D80INIf/hB7G3SGPtvboQLr7+K9Ocpz76EnKefox99CsV0G3m3tkIGNT0p0O9yCJUTVtcAbBtC6b10+tRkEqZjZO2p4bUJdmOkt2yFrHsewAkOmJRnn0EKfzvnGZpvPubRmsgyZN3UEBWtW+HQXSRpN7dgKFcXKS5BOOzmh30M7Q57BiDRyQfrQ4MQRbMtkA+RXC5zd8FQxt+TG9XHjqZtTO2YNUulOWsVLGjOfObMmSbvrShFhRIC35b4Xq8/VrsukG3NszWSRK6Uh375zVcxpu8ApMTEYnfuMRzKpflJz8GWiNUYPn064qhNG+vXwSKaYsW7q6m1q8ie1/BiN3l5YHOt+jhwy21IevhRHHv6KSSROKW+9ALSaHKPsYOTnnoC2c8/S59LQvTAfUghUz7WqB5yWzXDqQfuQsVdd6DklpbIb9jIhEOpHDRJ9JPStDj67cTA2pQw40Pj+Ns5ZOPHGRad8PDFSb9w5DDmzeLr6XxMDvBBCs1qap1QpNSvjZQGdVB0670ov+dRnH7yeZykZh+/90HENG2JxMYtkd26DQ4+9ypSHngE5x98mEy8NnYF1SKvIEGrTd/NCGEzf2ujJ3kHY2kjJJiKHJbQLS1wdcCWRx/C1tmDsfTAChxOikdZWhGyjxdiR+wR9Bk20FTE+JOfqP7MahbAFi+SXK1dE2SLbEk7dXAdWLGdkh0yJb0H9DYT7skJiSgqLkVMbhaitq7FimlTMeXNT7GM2jSfgC5wdUS0H8MNao188TpPN2zz88E+ak3ifQ8gjaYwlQAnP/MUtedppAjwxx5D1pNPII9muphadeLOu3CsQX0DXnK9Wshu2hDHabazaY4V62ZRMzPIxlPpy1OCGVqFUYtpQbKD69JEhxnzKrOcWKsWEoPJssNuQl79xvx+I6SE12UsXBuHqOV7gkPpX0OxnW4jskETLKh/M+Y0uhlT6zfAiKAg9HV0xo817PGzgwu6evthCgdV5n334Dj9aMq99+HQbXciQVpeOwh76tfFjvBQbOEg28jYW7KWRG2l+AgH/kICOO2e27Ft3iSs2bsJh9LTcLbsHGJ3HUbckaOmwFCZMVWdKCVq+WNL2f4ykNUs8qX8q0iBctGaA161YTni8jOQlUP2l12BXfsPoe/UQZj85UeIDrgJc50dMY+xbxQvaj3jWs3yRLs4YjVf3+LliZh72iD5sUdocp9C2nME+KnHaZqfoLmmyaZvzLz7HuTcfS/yb7sL2U2aIYVgJtCHJgYwTg6h1pJQJZPtphPcTIIioA2gjG3TKDk0z8UhdXCC4MXx9VX+Ppjl54tx7r6Y4B6IHiSA3ShdKB0o7RjufG9XDd/dUB3f3VgDH3FgvBMShLfCgvFBw7r4nCHb17c0Q9vbbkEHcoZ3OWg7ujth/313IPnBexBPYriPFufoA20Qe3szxN3RCgf4nR2MCLbwnDf5+GCDjzdW0Yqt4ECfx99c5eKPZY8/jaWR07Ap9SCSD8fjdGwOTuaWGX8sIqaZuyAOMNWQ2YayfwnIVpOTFwtUuaoIwbvvvmt8ybHkGBxMjUd2xWns3xOL+ZOnY+wPX2Jkg1rY7uiGZdTW5bwoafE6du4aD3cD8CY+7mEIkkZzlf7ko0h/6jGkPPYQjt1/L5JJ5tLuuAuZt9+FhLoNkFynIdJp1jNDKv2qtDVTMS+1NZlalEKglfXS/1ZOOo3/S9KpKflkz+kEWzNUfRyr42uy4A+orR9Sw9+8oznebdMKH9L0f/L4/fji2cfw9SvP4fu3X8EP77+BTp98hHYfvY/vP3wP7b/4BN3afYPendrhpy7t0adrB/Sk1ekYFoTt996J2Ltvw2H67N1t7sHhB+5B0oN34tgDd+Mw4/hf6tXGZoZxG0j0FD2sdnfDCjcXrHWyx04nb8xy9sGSzz7Cuqh52L6Z5DWrAKnHjptCQvln8R5Zz6eeesposZRPiveXgiz2p9plH45ElcTKZIvy5+dmIiE+1piZaUsWY9wXX2PVLfchslpNrPKxpwZ7Yin90HLGiqvpmzSSd/Jij4SH0/TS3D7yIDIevA/J99yFuNta4UhTxrQ0v4cZz8bQfMaTnGl2KcWXMS6Byub/ufSxxwlSVlCISWemUgRoKpmtkUuv6b3DQe44rkwZw6d1NNNfejriw/tvw2evv4K273+Ktp2/R4fu7dGlTzd0H9ALPQf3Re/hg9B37HAMnDgaU2bNwrQ5czCTA1oya8ECzFy0ENMXL8S0RQsw+puv8TW1eeMtzZF2W2tkteEgve9BJJCFp7Wh+ZZGt26JPSSCWzngNhHkDQRZ/jnamYQs0Bszb7RDtJcvIhmzr37rfRzcuQEbEg4ilkRMBQYisJq9U1asWrVqplBQ2iwidl0gWx/6M7HMtabDlI1RjlV+Ys2aNaZ6MS87B4XpmVgatQijhvTGVBKodXaeOMyROcu/Opa6uBqJJtCajN/Fjj/EOPMoQ6Y4mV5ahcMMwfbXqVMpfH0/NfUQQY3zC0UaNTONYEp7s/jacT/Gunye5h/E0IfM2L/SXCvdKR+cTPOaEkZTfel7u0JdkFmLwPN7G+rUw1ueDvjsrecI6I8YMWEyFs+ajaj5i7Aicimio1Zi5dJoE/atid6AdWs3IzJ6NdZu2ortu/Zi954D2E13tOdIDPZSw3bFxSFi6GB87O2BKKVXGVrlNGmJrNZ3IrFFC2TSL6fdeTsSmjfDQV7b7oAg7PDxwzYvn0oyxj6JdHPAnCBXRPl6sN/ssS68KZZzwK3Yuw57c1LMRIVmrxRWKQ+hpJNm9hTdqFk4Xa1dE2SJaP23335r4mGZDZXGqvBdsXBichaO7t+PGX16YNJzT2AeO3iFXQ3sYhixwN0RKxgPrya4G8kytzFG/IXA7KVPPcDHA/z/CMHax3hSOeqDBOwogYml1h3zDyVbrosMHi9DJpjmOYOdlM7PaGLiGIFNCKYGC0xetAhVxs0NK6VhfaTXZjhEn7wnzBX5JD459L/bg2rj9Wp2+Oa9lzF87GgsWrgMh7ZsR8yuPTh28ChSj8Qh5UgCUo8eQ2Z8GnKPZSMtI92ENkV5+cg/noPcrMoC/dTjmYjLSMEvKyLxSZAPFgZyYBMocYS42nWwR8y+cX0kN26KhHoNcZjM/yCv64BfEHb7BmAr+2Q9gV7m6IjN9cIxx8nOaPkqhl7jGzXDholjsX7fFqNI0lqBrClK9f9NN91k8FAWzMLoau2aIMskaKJBa4PkD7SiQOZDwGuE7TpyDFNnT8fM7z7HNAI3n0RiWRi12M0Jmx19Ee1JH8TRu4nsUuU0Wzy8sNvH14AZT00+TlYrUxrL7ybQlybRDGsK8Zh3gJkxSiZZ00REGkmLpgTlf1MJbGrtWkglcxWzVqozj+Yy//ZWyCMhym7WmHFyXfO5/SGuKA8JQ4VzMI4G1MeHPL/en7yLBTTDu9dsR8rxFGTlZyG/tBAFZUXILS1CTkkx8ivKUayChtICnC4vwbmyEpwpKsTZ4iKcPVmG8pMlyC3OxfZ1y/BuiAcWhXojIcQXKUEBOMTBt54h0o4QbySG1yPhq4XDtCSyTgcpexnmbWMfyDfvYxgXxXNa4euCubXcMY/h5UrPOoh85nVs3b7G5B6OHTtm+JDSxaoukTYrv60ZKwunqzUDsm3aUmGS9b/eO0OG17r17aaqsh191/JlEUg6dADl+blIS03Ez4tmYmLbdljS5DasdXDDOueaWONL/+vthdWOJFyeHljPsGEntfYQtdAk99URJD/7A32gGaUE/n8kyA8H/L2x198LBxmvxgYT6LAQEwrF09/G12HI06I5MjVDdMdtSKHrSCfLz2xzF9JIeDLb3IHce+5AdsumyAhneEWTXhIYjmy/OkgNpQUIo5UICMUPZM79PvkU4ydPQdqRRGMOlTZULkC5ds2m6VGhohI/1qNe06NExEcTC8rsHdi+Dd81a4IxTg5mEiTGyx9xJIdJtCKpwYEEltdGa3WElkhymFZoP/tClmsXB/xBn0As9XDFdF9nLOb1r3IkyDVcsK5JI+x57wXM2bseRzfvBJLzcTAhHot2b8E3336Pe269HbWaNzLKZpsYkXuV2AJvZwuwRc1tX5sybpIxDw+Q+fYfPAA7d2zBcZKsrKwMrN+6EVPHDcPE117HnMBaWOXghPUejmTTzliuuWEXP6yhCZIvFsgy0ZoPPkLNPEBQ99HMHabp1tTdQYYpB+nXDtcNR3zDeki9qRGOk4Sl00cfq0WAqa05t9+GHBK0jLvvNJJzzz0G3Iy7b8fxuwnyXbchq3lj853j7NhCdvZxf2q8AdmfbiAUnehKfvzgQ4ybNBHJB+NMJwlkgfnfgHxo7y60v/1WjHR15DWE4IiyXSSHRxg5xBK0g7xmDW6BK5AP8VH9oJSnplD3uDP6IMjz/N2xMsQfmz18sbqmGxYxKpnfqgEWzpyGZLqT9Jg4pJ7IpQnfjX79B+LVF19CncYNMIsWSQNUoApcKytmmx2zsx0FuhA1C2Qxuwceuh9NaP7k9GfOnE3TkYLcgkLsj0/A2NmzsfK7LzHj1lsxrQaZtH11arELFns7GLK1yS3AALyV5log72PHH9aF0pwdos86wNhTROsA/efBBvVxpMnNiG/ZHCm3tkIWNTX7ttuRShaeXr8Bclq2QB61NvfeuwnoXUby7r3PgJx11+3IJsg5ZOdZNzcyfjqTJjGPnW2BnBHqjwRqcg+Sm25vvIkxE8Yjft8hk4aVKbTAFIC2IFsAW2J9Rt8RyCo87PnYwxjm6YzDZNBH6W4OhtXGfsa0h2mtBKqu+Qjdk0TPxT32sU8E9C4PH1o9T0QGeGJNkD92qNzJ0QPzHZ0wxtUeEW9+ioT1a7Bu72bkVpTg8OGjWLhwMbp274aWLZqZJToy5wJVINsqqqXNxlxbzQJZX9CHRdVDGobj6WefwoTR47Fh9Ubk5BYiKTMbqzdvw6Dho7CRGrWEFzT/hhuwxLGamVGKINDL3MimXf2xnRciRrmLF3YgSB1QC0fJcmOVuWrYgCFTMxxt3gJHNbtDTU2iCU6+g0IXkdzyVlMokK4Kj1tvwQn+lsBM5+cyVRhw590G4OMUaXk2zbkIWCpNvAiatFlxdWpoALJC6S9pvvtXc0bbZ57ByHGjcJiMWTsOaJGbBagAtAXXerTAt/4XyAor41Posl5/CYO8XXG0aWPEN2iEmJtuxqG6tXG0bhhiOICN0IUootAgEPAy2QJ5j68f42cfrKSbUnJENeNb3fi/5qFdHLEksCk2DP8ZEbuXIzkvHelHk/ALNXv8jKl49uFHDNNWaGtFQRaef9Bk/bE014q91ESqtHirbqtGaPvDt1jPsOLI/hjkFFVgm5anTJuJER26mzBgg2JgN2csdXVAhJcTlvOE1/r6mArL3Z66EJonjuKDNLsx7IT4Jk1xrEVLpNzSCgm334l4ghp3512Ib9PGxJfxt92B2Ga34HDDJiQuoUin6c5ufQtyGI5k0DRqgj+zdWtqOk31HXykjz7B/48zHBPTVlrT1G2xMw3IYX7ICvYxs0/Danrg8wcfwPAxw7B36zYTIQgsC9iqIFsAVwVZJl6DIzY9GcM/fAe9XOxxtEVTMmqy6ZatENP4ZiQ2bYBEupnERrxmRiZxdesaoKXNiiwUVez28TZTlBto2pe7OmOVkxM2uiufQLNNK7jWzgtzX38ea7YsxJ6Du3A8PhUJiSmIXBWNtp98ZhJTIsSyKlazdbdqBmTbCQhL3VUXrRmQu5+6HyNGDMOhX/YhOSUTGTxYRMQizOjUDeOfeo3hEqk/GaFKX6I4Epe4uRmAN/n5kmG6Yj+JiBjlIYYQR+s3RFyT5khq1ZrEiYTpjjZIvOMOo8ExBCqWIOr/hFa3Uhua4FAthiD165jyHZni47e3NiAfpxbLlGeTfKQR/OzbWtOc34JMmnVpsUIsJUJMlSath0DOCfLGiZBaGM/4/X1+d8jIwdi2YYNZk6UOsgXQAtkC96ogZ6Rh3JefoUN1O+xvejPiGjcxIB+++WYkkx8kN2+KJGp4AgdqbL16l0GWr94vC0diupv+dyfJpnL6y91csIL9torMe43m1au7YHqThtgyYQi2rF6OY4nJSExJx7btOzF60GCTmKrDGFyFG9bUo4XnZXNtqbn1qKZpL2VXVG359ufvYvGiBUiimYhLTcfB9CRMGzsMs996D1NCGmO1U3Ws9iFpIJla7u2Pla6M/9y8TOHdRh+aME8/sssQxNSqy9HdGHEEI7H1HUi+rY2RpNa3Ip6mOPaWFuYxqVUrJNF8x9VvhMOa3G9MLeZ7xxkaZfB9gZpDk36CFiC7ZWskM3Q6zu/kNmuBTP6GkiNKlCgDlkI3kk7zmMqQLifYE/khtTHFPQCvs+MHDR+ITWtXmblc2yWuVwJZZMwiZNZ7+l/kK/lEDmZ2aId2HOz7mtxkBmdc81Y4QBeTSIYcJ+D5emyjBnRTdXCYcb/hJTxPMe99vl7UaFo8avTWAG+abQ9E0PRrlkrFjJs8nbCIYC959jlsWTAPv8QdxdEEAh2TgEgqm+JlEWOttRK/ULPAthT2MvGyddSqjdZ2DY8++ii69O6MbZs2IiM5E3sTE7H+8C6M798Ti556DjPsaKo1Ee7thoU0Lct8grHOjTGivQc2uLhga5An4rwCTEgRV6c+YpuwA25pjbhb70QCJbHVXQT6VlMEEEsSIdJ1rEWLygRCLY56EqWMpjfhOF9XnVYSzWFKqxY0z2TSNNWZNOl6LYv+PKcp/TEJTwo7TiCnU6OV4jSPBDmX51IYUgczPALxKk3oz8MGkGOsNCArqXC9IFuEzAI5t6wckb17oP0NdjhENxTXqIlxNQdoBRMa34QjNzXA4Ub1DSk7xNhe2T5DxCQ8T82oqXxoN4HcIr/M2HphIC2ir7spQ17mV51xdDWy9wBsmTkdqw7twb6YeOSlZ2P9lg1m8kJxs/DSggU1C0sLWzuhLS3WixbIIlxy6JrLXDFoIPamJ2LL/kSkpRVgzLwJWNHpayywd8d6xqJRPDnJUppqmetoTwLv7YGdjI1FLOR7DrGjY+rXoylrzA5oZiShWUtq7C2Ip1+OIUDGjPP/Y01bIr5+Y2o+R344zTW1TpLavJmRNH43sznj5UsiQiZzLgau0CmNYYxEjFqzVMeD65v0p4r+EsODEMWO+4E8YvQP7RG5cYOZAJDZlYkTcBbA1nMBawu89ZoFfhpN/fqF8/G23Q3kHE2QWe9mbK3HePiWm/ibNNEkmEZoquWTlcqNoXU5yr4TyAcDfM2iO5ns7XRzG33IsqnZK80slTsO3FgTg+q5YqmDO7Y//hQO7Y7G9n1bcCg1FVnbEjB58mQ8//zzpnhDiwmvFC39IU5WU3JAtFw5ak0lbpoyCbvSlKdNw+6dhzFx6nDMeetFRJLmr/HywzJPz0qgCfAy+hSBrGIAW5APM6QRyPHUZAvk+KYtkEig41vc8keQG9NUs6MEsuRYsyZGUgikRCBLMi5J7i0tkc3jKsOlNKY0V5LGuFvJiIzgegbkzLAgJIUFMob3xA8M74Z99w0WrKncyknESxr634AsjrI+YiHevrE6dgU3REbdm7C9PkMlgpwR1sAArGsXyLE019cCeRPNt0DWVKSA3uvihlFBjoikC4y+uSUORM7Axj3rEZeWivjdCabYTxvhCC+VYCkFW7Vd9slW09phrbzTpidap7M3ejn2pCXhYGo2IudFYtrAHhh7R0tEOfJEnL1NbtoCWSNPy0fkj3/x8TGxoAWyLjahKc0ZtU8gC9R4ivlfZpyE5RjBTqKpjq3XkFrMUU/iJdIiOdaEJKYpweZnU3mc9Etygt8/zpBFxfQiWWYKkpIRRN9MkNODahs/naW555AArKYmt3NwwsCPP8bs5UuNa5JPFngiUxaQAtAC+GogZxUUYVNUJN5xccY6Hw6yOo2wpwHJVfOGSAmtZwA2Qi2+EsiH/KkIBFkF+zvYZwJ5vbfXZZC30JTPcrfHQp53hL0vdnbthHW7oknAkrAnNhHbtm0zOxjJZGt/FeEni2yLq2HXCpvkrCVjx441IGu+WNNbh3btwEGOml8Sj2Em35vx2fuYWCcMSx1dscbBCysJsoAWwDqpdV6e2EYSofhPDNJMQlDDFBMnEtx4+lyjzY0pNxN0AmUIGUFOatKM4QZNem2O/rC6SKzdwPg1SRJDEskxsVZ+3hS/U3L4veNk7Rk01aZogB13nB2YGRhoRJWYImHHVdYT5Gcm7tvXtMdPb7+FGSQuWnEo6yXgbEHW8+sBObegGFvXrMCHwf5Y7OaLVFqfg/Vr4XBjDlBygKO0MEYIsOJl1W7bgnyYUYm1uG6nr68BVSBLWZQS1uyUluHMZXSw0s4Rq+9/FBs2RyB2/15sY/im3Lbm9bXcRnuZaQq4avtDCKUUn4rzxKq1IEuzTfsPHkBsShqid+7AzGGDMO3+exFJQhPp6IT1zr4G5Epx5+jzNCcos6Ngv3KkBpqwIb5Rw8uaHCNgCbCYqOLJGLLDeIKXRMASyapjwjXi6yCpDmPLmxh2URSCSBRzJvHzyZfEaHHd+ibLlc7fU6ZLQFsgq5IkMyyE7NqfcbIf9nHAda5eAz1eehlTF8wxBYdipQJMCY7/FOTC/BLs2rYBXzZrgClOrjgWyiiiXm0calQH8WTzR+rWqZRLSZGqIGuNlUCWNu/STB0VRCVCMtmaf5/t4YhfvHwxJ8gdW2t6IsIrDFsWT8L2TeuwKyvVLGCQyVbRn3YaFH46dzULV0O8LGetUa0yE+1Y17FjR1Pzu/fQUSTGpWBuVASm9+2GqeG1qA0BWOTiiHWeAYzn6Cs8JRxxPCmdoEakmTbkheiCYmlKBbJ8sgA+Qg0UyNLkmJsbmvDCAjCevstoPsFJqUe/3JCkhe/LEkgSGJpIzJIYfl7ZsJQ6DJ0UMolZ03ooCWKZa2XMsjTVGMQwipqsc+lWrQa6PPUUJs+Zjr17K7Ne6piqIAvIa4FcmleKIwd3ocNDd2Ckvb2pHYsTi1YOPpQhU53aRgTyUZLZqiDH0rIcUYG+TDatn0DezD6URVR/Kuu1k+8v9HXGLwxRF9k5YVOfbli5OgJHGe2IU6j2XZvTKXmlpTYauGqXQyj9sUDW0lKxagXY2qNS9n5/TBIS9sRj8sypZKSfYqazK35x98RixsCrvCtBrhQRMS9T+bHbz88k5OMCKqcT4wmCAIqltipJIBHIcTTZAlGiQSCQNRer1F8cO0MgH23AUIpisdQ4hibKHiVeEhX2JbEDNbec6Odvaqg1HakpPxEvLVYT6TrO+POEvzeO0Wz2uKEaOrBDJs2YYqpblPUSwP8NyCfzypCQeBi9XnkMg2pWN+Gi1k4dqMVrJ68wYRPlMPv1aiBrrZdAll8WyKZMiLKUfb06wAdR9MsrtUa6OiOYV17FwnULcGznfrOHmUy2dhXUboGqvZsxY4bB02oGZJ20TlgUXFku1XFpglpO/FB8GuK2HML4KePQ/8OXMZ+UfkfNmogI9CSb9sMa90qQNep0UlupxXtofgRyfGAoEmvVQUJdhg8E+ShBPEQTKxHIIl62IEs7pWlK5AvkNBKwI/XpzwiyRWAEsiThkiTyPWmO1iXFs5O0TiqZZjpZU5WhgTSHvkhT7trbFbm+nkbre9jdiHb334cJUyea1R7KeinmlfynIJ86UYaU1Dj0/+AFDKhRzVSzaF5cky9xtesxrAq/DLJKnmJCwv4N5BiCLJNtgbyFYoG8zodaTIVa4VQDszyqYbOHPyJa340FWxYhfTWVcP9+U8ChUEp+WdU72ghW0YLV7KxctdiYVr/LrmslvOy8iryTDiViJmOzaQM6Yf3dbTDHwRHr6W9XVnPGchKd5YyJjS8myDt4UponlenR3OoRdrQFyp9JDIE9So08woGg3PYBgns4lKCxg5Lknxn/SmKp1ZL4ujTZNpLE+DeBPld11uow1VcbsNlxx6jRSeIGSjxw4CgDFktzOJtgv1k3DGNmTzPVp6q+MIARPAtcC8xrydmSUqRmpeHnDz/CgGpuSA6pi/hgHwKr2afKVZCWHA0JNqKdDyw5TA5hfDPPUS7O+GX2oypZJREMoZYwSolxD8ZU9xsQ7eOEfd7hWDW4L7Zs3W4AFk5avaKqEVXSalWLms5PTNtOfwS0wgjFWto2WIXcxh/TXyUcoKneGIEpvdpiZdOWmOfkbAoCVtVggE7/Vwmyhxl5AtnExjYgy8deTSwNFTk5xJFuMkIMBaTRifU4CBh6WKJkgky/JLF2paimS35Xi8+0blkAmzXKl0BWjKxChaMEWGY8luc4m/H86zThwyePN1UumqoTuOqU/xbkIZ9/jn7VXA2jvhLIFsBXAtlo9Z+AHOXqgUia78NugUaTo7zsscXFH0s7dzBLkkS85IO13KZHjx5m646QkBCT4FEzIOuJEiIaDd48mLRZhXoa4QI5du9RjIychkntPsViXsBiVxdTebnGsXKE2YKsBIiJjQly5V4d1ORL4PyZWACKmMicKTumuDo+vLZZQajY0sSX9NUSLSGVC5AkkYUnU4uT2VmqMKlc0upL7fVDCn1zKoGWqLJEQKuEKJ5RwDzK62S1g0cONbVT1hbHAu2/ATmFII9p1xa9bnQyjFogHw4PREyA/K/I579Eg80SFRZUBVl8xgJ5K5VJkctiWsnd7gzRPB2xxKumqRxZ9OZbWBu93IAsnyy8RowYgfvuuw8uLi5mWc1lkC1zLR+s9cUiXarU15cE/JFdhzBo+ghM/vgdzHMhAfB0w1IvV6x3Ywzn5X0ZZLFqJUAUOh1kZwrkw0Hs1Eua92cSW4emWULfdYQjXqNbHaCVg1rpEM/XJQkcABItCj9mI8pPywyb+msCm0yAU3kOEm0RIVHRXzwjgiyRMgIcyfN9288HgwYNNMxUqU35Y4FrC7IerwSsrZzm95Kz0zG1Vw90sbOnz1Wyg5pHHhBXBWRbcC8LTbVAVs2b5pkF8nb2pQCWrCXAC93dscndz2QTF/nUxOIba2Bxm4ewbtkiA7K4k6pntWWWUpwqwl+4cKHB1YBsxVLaa0pTVkqNabdXmQI59QM79mDA6H6Y8soLWERmp4RHpB9BZti0gidjgSxWLVOj0haZ6usFOYYsVBIXHoLYEHUCzStF2zeoxFb+ViKTbO00oLoviaYVpZ0qoE9j56Tz9yUZPr7IokWR6Lkp2aWpzuVnsjkwNRX6MUEe0reytFgMVanN/xrk3AzMHzYIP9hVx8HgMF6DF82yLEs4Yi9ZEUu0hNYSsWqBHENuY4Es0mqBrHBKqz8XuKlmXTsREXB/Jyyyd8Dy2i2wPnKWIY6yuBqsU6dONRu6aiM8abU1J2FnTUtpykoV+loErSSIQJYm79q0HQMH98bUxx5FdDVPM9+5NMTLrITQRmnKV6t8xQqdZHIqAb4EMs3u1SQmtHImJp5ACliFPCJQ8rFJHP2JHJUSaaNEgGm+2JIMgiXJJHBZPAdJ9iXJ4fll0fSZAnx2aD4/k88O3EKN/oLvDene3awHVmpTc8oiX/8pyKfKSnHsRAaiJo/Dt3Y3YB+Z8xGCfCSY4ZwvNZm/XRXgSnArRVoskMW4NTslkMWwBbBkq5snFtE9LvHyxQ5nT8wLcMJiDxesd66F1XPGmmU0On+ZbO1OoF2FtWOv5h0ug6w/AlqZEjEzZU5kuq2NwbesXoefB3TD5LvvwZaaPohwqImo2t5Y5uJkCJgFslbv6QRNTZMNyCZkuKpIaxn+EOwEhUGBqnYkOybQx3gM+VotK5XIJFt+VkkPSSZBk1jg5lJzBe4JsnyJNNdkv3huBXxeRNEC98+pHQPbtTdLR5UEEvH8b0FOysvEmrnT8YWdHfaIUzC8PEqQU3zCOWg1cBUqif1XikImSwRybFDon4K83V3Ei0DzunbV8MAMP/plxvxb7LywfMIAo8HaHkoarWtRLZ4Wr2vPlssgS4t1gVo5J03WfluqAJSplvNevzwaP/frikmtbscv9v5YWN0OUXV8sNihmkl+2IK8l0BUBdmEL1cRld4K5ERqsUAWwPF+vgbkawFsfC7P4ThBtgDOI7s3QoAlBmhaAoFcSBdT7OZutqn4zNEJfdghy5dXkhfNKVcF2QqrriqXQN6weA4+JchahH4oyJPaTG7AUOeqANOiVAVZfWiBrPSwEk9Lfb0xnwPzFztXTPW1N4vZt9k5YtHwroY7KaEjt6PqEO3BrX3TnnnmGZPxMiAr26WLESvTzj3dunUzxfQ76cwFdNTCZRg0uBtWuQQSUBes8HBGpC/BdXDGUpoNMe11PClph4rmNSlh/BAvVsl3+VhLjK8lcBIRIUkCO0OiBeOJAQSXpjeJksgLk2S4uSKDfj/Vz5OveyDBx41m0B2Zfl7I4/Hz2BnZPl7m/wyKHnP5WiG1uZwmLoOf1WOphzdyfNyR5+uGo+7e+KGGAzp987FxTdIEDXQBLHAFnvpEfOXfQK0iF0tOI7XoBDYtW4zPpMle2mLKE4f9vUx1qHWd1nWrD2z7RGu9tGJEJjuWlk1Koihlq/IO7NcNMs0ungTZF6vdvLHEkxaU17HajgS465cGJ4lCQYW9CqOUlpbC6noMyGLXYpaKj7Xvh26ioUSIzLVAXrogCj8P6noZ5JWeLgZkbaq2zMutEmSCcS2QdXEWyNaFWyALYCP+DH8kBDmZo/gYwcrg8TN9PJEV4IP0AG8DdrqPB82wp/GvxQSzwGgxTTbP6zitSi7fyyNhKaQWZLJDKkhaSt28DMj5fu6XQe781Uf/JyAf86e5JshJNiBLLJBtgb4WyBs9SXIvg+x5GeRV1OolXb4w4Cr9rFDQFmSZbIWFl821ptq095bUvE+fPgZkmQGBHDlvCQYM7IyVTv6XQV6qElJnAqxtEK8DZFuAbUFWVkrgHuPnjBBgSQrNdQovUCLQjvtRCzkYJLkcACfIBSwQBV6Ju5cxxXk03eY9Pubzf72WzQFxkiCX8XN5fF5AzY718EH7avbo/MWHJtSQT5a5Fsi25T3/KcjyyXs9GaMT5BhaFK3n0jXaglwV6GuBvImKtMHVCwtIapVCjvRwJOF1Q7Qdla3z54YgKxEiAiaQpaQCWRMVihgug6xZGKUzBbJCKYGskSFqvnhOBPoP6GRAjvJ0vgxytIs7VvqSbV8D5BiRL0qcCBVFxEr+1hLt12FJqr+fCYVSOWqtcEgAC1gDLrVbWivtLaGvlRkucfFAqaunAVqvlfAcbEX56tMevqhgR+UT5CJfDyS6+6LDjQT5s/cNydT2SlcCWc+vBKytXCg9jZTif4G8jyDL2sTThaT6EWSbazWk8pJY/XItkDdTkTbSTGtHAgvkaC9XrLRzRkSnzwzxElYWyNJkzUYpe6mpYzUDslbpKbaSubZA1pfE2ARyv/4dscJRJbeVIEfRd65y9TD7Q18LZOuiqgKsHfUkAtYCV5JByeSxLDEA0xRLQ6WdYselfL2Cv3OS4VoZTW85NVMm+SRN5Slvf1R4+6GU5EuDIY+m/gxBrWD4UcR4vpznnOzhh052NdHxo3cwb948kxa01kRZINua7quJLchfEuQDPLb2H0kgyBk+oZev80pAmz75H0G2NFlAr1u3zoCszeJVj63Ztcsga9Jc88giXn379jUg68uy9ZYmVwX5ejW5qnk2JvqSKK9cFVxlpY77Voo2NM2j6Ra5knkuoAi4Yn6uiB1WQCni90r4HYF+2icApwh0BUEvpv8q4kAsYGefI8inCHIxQT5Fy5Dq4W/WRHV67y3MmTPHuCULZJno/wTk82X/DrI4hLaOyvIONddoXa91/bbm+68w1/LLlk/WqlMLZCmvAVk0W6RDjlrkS/dcskDWFyPmRv6puV5B83ctkK0LskC2BVhiMlSXALYF1+xYS5F5tuJbY4IJaD41X9qS6O2KNJ6DOlUkTOZb/veUu4/xwSU040U8t/NuvjjtRP/t6YEztAxp7v7oIpDffdNsTaWkj7Xw7X8F+aA7r4M8IoUgZ3tVglwVaNs++V+Jl7RXOAkvhVEC+d/MtUIozVhoVz2JPqQQSuZa9t6WeNmCLOL1n4BsjWJbgCVKO1qmWQDn+FgaHGjEYtBlPtRQxplljHmzA30RRxK4x7U6DrjUQKyrgwm1ijy8DMhn6Bcl0uoSWoGLbvzf0RPl7DiBnOkegK4EueM7b5icgLjHlUC+EqhVxRbkr21ATqVryfH8F8gSC+Q/AH0NkP8YQv0LZCuEErDCSWBrK2XhpyJMi3gZkPVHIKuQXrGV5pJVGCaQNUKWLZyHwX06YlbrO7D7Rk+scXHAomBnrOIPKRESqT0k+Vwbnqri0JjokCCT5DjMwaCKDZOiJEDKH2ufrTRKOk2rJNOHYPqGoMgtEIWOJE4u/ijzD0d+aBhSyT7Tgh1RwePDPRhwqINku0CM4wW+zhHepkEAvnOyw5ogd2RXvxGgKUYdWgAHO5TTV8OtFr8ThgoHT1yg2T9tXwNgp8ZzEP3Az/T/6BlDvJT0kWmzgBXQ8s3XA/SZ8hIk52ViZ9RSfGVXDducyB3q0Be71jQDTpk4pVxlsUQoNUOmChbtYCRRwqfST2tZr58pz/2Flk3bX6l4YJM7Q1X5Y89grHDxwUL29y766Dl29tgyoKNJgFi7+C1kpNCuXTuzpbKqN/8QQuliVFUgTVYOWyZM9l0mYPmiBfi5fydMu/0ObK/mZbZnigx2wRqyVM1IyUcI5C0EVLXDFsjxBMiATIAtkFUaa4Gc4Uvx5ggnO87mewW1aiE3KNBkrhT6CCT50UJaCWl3glcgVvMih9q74St21GdtbkPb919Fp4YhmMa4OMPTH+edXJFf3Q553s4oIKjZ9jTPPuE44e5BK+CHMgcHnNMO9C6++JSA9/ywsgJGBFP1ymLUBjgb8lUV1KpypqIcKYXZ+GVZFL6xq47d9P8navEavZzpPvwv59MzaFEEsmbKbIH+M5CV7VLlpm7HsITfX+UeaJYgLaI13ebsjjnVXbB5SDejveJOwkskUviJWynjpetQsxPSuiAVaEuTVaWpm1Eqca8vRkdEYuCgHhjf5m5srOFJM+2AJUEuNNGeZspxiXslyBp5Ws9zmNqsKg2BfJSv6SIkSk1aKUmlIzMpWdRkZasU/6aH+iA50MNkqEr5Gkhg4ELT51cf6139MMDVCR+GeeONNs3Mxi49O3yLkf37ou1996JtTRfEhN6McwF1kXPDjTgZ4o/TtUNxtnZtFJJN57DDz4b44QIHy+mAhljtGY4vgsIxoGt7k9SXT7aWytjmqy3QryYVFWWMk3OwjcrwHc31IS9eU7A3svw9UMzQTddXVZttgVa/iHULZFVuVhba/wtkpTc1n6y9vtbQFUT6uGFtTSfM4WDaMLavMdMSabRquzT3oJyH1kZZ08gGZCEuVm0tnNLdX5TT1ShZvWQFBo7og9GPPGS2h1juZI9lgS7YQLITSZCj3Byhndt1MrsY5hwisAJZW0TEKHtFU2OK6ygWwAJXkkNTXUAQcml6swligYc7TvG7p8LCUKiwysUZM7zq4Cffyo3SXn/ufnzZ40v0nzQck6dPw5zJMzCqd3d8Fl4HMx19ybbr41eGUGdpbYqq2eGkUw2UOdoZAQflbwytsjyCMcnBGx/Ub4i+/SpJppIhclka7ALWAvd6JihKy8uMJkePH4uOBDmBv5/p60Le4MWIgHE6+yWH/WJSr1WAVr8IZPWR/LNAVsGFQJY/1nTjdr6nqcZoJ2+sI7BL/TywrLoDFobVx7oZI421FcC6Dt0y+KOPPjI4ao2UJigkxlwLZC1k1k2hNVGhAnuRL2nzmiWrMXDSMIx46Vks9ww0twtY6eeKjdReVWwKZC251Antptk8xEedsECOFcgEXRksXZhYtOZ4Dbm6JDkeTvTDrviVJhQe9J80r/v5menu9vjR2Q6vhtfDJ/fci26ffoTBA/tgwrRJWMhz27B+K/Zu34eN21djyAdv0VTSN1dzAOo3pm8OoD92oT+mDw5wI8CufI3kjSHYFkc39HB2xkdtWmPohEGmk3TzD5lm9cN/A3JqXhZm9OiGAfTJGrhyF/lBdDt0R/kkYYrVBbYFtPpB/SHRxIuJo/kYQ4BtqzZNhQj7cp6rO9Y4kYQxlIryd0dEtZpY3qoNVkVOM+cvgCXCzbq5iwoIRKqlzQZklf+oEMzd3d2U/6i8U2ZMI2RN5HoMnj0Ooz98CytC6mIpQY6m+VP8plrgZQR5LX2zTkgr8w4S6Mu+RulJXy8j6fStmhK0ZovyCWQBpZim9WRgECoYbqQ4+GMZQ4Ofajjiw1A/vNG6Id58/2107Nje3GlmxfzF+GX1JsTtPoLMhAzkpp/AvoQD2LR4Drq3bomBDo5mAXecowtOhYTTKgSjlP5OcXasixstkTOGu7vhmwZ10PnTtzF76WzDrLXgXoBZmmw9vx6QKypOIfV4Ogayc6fWdEU5QT7l64Yif3IK/1ooNOHdH4G2zLcBm+BKowWyihAFskqoVGWjPlXR5Hw3rRL1w3pGNAJ5kb0TNj7xIpatXlgZAUVGGqX8+eefzZ3bVRkiK3wZZKsAW9NtKv8RK+vdu7chX5qfXBO5EUMXT8e4tp9jRYMmWOrqjBVuDthIar+AIC93dzKboYry7+IgOchjmGpJZXekwbzANF5YOjVcIGuuV1OBAriQ5rOoTi36I3+zAmM4CdJ31ML3mrXE56+8hk6dumD01GmYvWAxT3ot9v1CQBJTUZ5XjJPF5SjOK0J69gnExR5F1IRx6MPQ4StnV8bA1TDC0RVDatpjJJnoMBK4HvTb39NyfNayOb5+7WUMHzYES1asMvVdSoQI1ErQKtOZYtgW4FeTCp5HUlICvml9GyKcvXGOhPE3WrkCd1dcIEco5vVXBdoCWWKBrPBKIOv+VgJ5F/tJhZGrvLxJtrQ1VhDWOzB89XMz9+vY8+6nWLJlueFNmmTRpgGaQdSmttpVNy4uzuBq2LVVGaL8tbZUVGpTc5IqJZFfXhO5CcOj5mJC1+8R3bSlAXmlqz02uDtjvr8LAa8EebsNyJXVkmSPMkm8OAFszfsK5HyCbABmpx9lZyywt0MX1xvwQdNwfPDKE/i+/Q/oN3g8xk2MQNS8pdj9yz5kEMzskmJklRThRFkxyipKDbM9fQrYticGuw8cxvxp09HjzXfR79mX0OH2Nni3Tj181rgVvmjaGu83b41Pn3oenTp0xIjJvLYNO7FlGy0CtdjWH0t7rXVh1wNyyYkSxB4+hDeCQxDt7IOzDm743cMVOTVq4LxvHQOyLdDGR9OqWaxbIBsi9icg6zZEi718sJ1Rwdqajman/wgPbxz6sh0idpAzUWNldRU+Kfy97bbb4MfjWIkQtcvmWqqtIj6FUaoukI8WJV+7cAUi5kagx9DeGPPCE1hJ37fN2w3zGvohurqbqe9aTdO90dPd3DNpH/+P5Qkm0iTr5E2oEFh5WwAlNSoc7I1vPOQXhlnOQXibWvYOidOnj1IL6XfbdeuMERPHGT6wbcMm4y+1C5G0TVOi8p0WGAJGACktq7JaTZ7L7Yg4KgevxIBmZWSZRCy1KkSL+GShFFtqilHHso5nAafjSqTR5Scr7+ZysrQMFUUlOH/yLE6XnsSF0+dxsvwMCrNPYS8H17c3klmHeqOCFuRiDQ+c4yDOC/SsnMvmoFa2TnPcKkHSRIuAliSTSCWRfOrGKUcp4jR7qDA7aaJ3uHpibpAvdrowqvFnn7szRLNzw7KwZlg7awoWrllmbtEkkT8W6dIKCtXpiXBdZtcy19Y/Wk8jZiawdXdQjY7oRcsRsTgKP47sj2kfvYMlDMK3kqnOCXHDenuaEwIun3wlkFUxmUf/kMyLTOboU565PMgfh2jOxnNUtyP437a5Dx2eeRb92rbDmBHDMVVugiboILUjOSHejEiryE4m1BJpmganwBDwCoF0i3oxZeVyBaTMmK5BI11+SxxD7+kz+qy10E3HvhLIlccuNR1WVlJKUCtw4dx5nD55hr9/ge+dRFpmHka99y76E+Tc8BAyehI9n2CUM9bPpSurCrLStFVBTiZnEcgq/LcFWXeOjaBC6LaBy4PcEeXqgB12JGH3P8GoZy6Wrok2AMsfDx482OQ6tAJG69hsm1mfbPllfUH7Z8qui4IrURC1eCkWLV2OnyeNwryO7eiHg025z1wPR2yheZIWC2RtbbyDYoF87BLI5WS1msRPJWkoCQgy2aAFng742N8Vrz54J7p+/y0G/dQbc2bMJGPegv0HjiAxKRk5J7JRXkGzXFZ2WWulWXq0BcN6LrCk1RoU2iJCvlZ+SSsMJKrIlFUQuEp86LMWuFVBto4tqSgrx/mz5yo/T99w9vw5ancFTvOxsLQEe2MO4qPQYCytURO/BYcbkC+E1UIJTW2ZUyXIFtAWyNJmy2wr9y5iqiqYK4G8iZYuki5xSZi38clr7Tyw/vtvELk+AiuWrTQYyR9r9kmkuWHDhmZQ2zZjrqUZ0gp1jCrw5Zdl37XPsoCfSS2YMH8OpjBMmHfXXZjlSt/gaI/tbr4GZPlkC+QDnh6IuwRyusIkl2ooZfyX7+mDEk+GT2Td05yr48Nwf3z63hvoPnIQxs2bjejNW3E07hiyj+dTw/JRWFyAsxf+BYBAlMYKdEss021pox71uiovBbbAlIbbit4TYPpuVWCvKBWVFqSkjK7iTAWKT5aisKwAZy6eRkHRCcyfPR5fOdZEgpMn4OKP0yRF+SSSJQwffyNImgK1QJa7sky2QJaoykXEVFUwuquc7lIjkFXAJ5APO/tivo8LQfbFthsZtjoFYu3EIZi5JQrL5y81GGkmTbfuVaZLxZha9iPrLBd8GWSZPktTtNjN3GPx3XeNnY+IXIAJ9M3zo6IxpHsPzGJMOtLFnqPKmYza22ixQNbNu34hyIZdkyjIRAvkdKcbcapWKMp8A1Hq5G7mere4eqA3WXS7+x5G32ljsXjLeuxPSEJGTj7K6OdKS8sJRiXI6mBLYy3ALQCs1y2ts0SfsQaE7evW96p+/2pysrwyl11K7S05W4H8Mp7jySLg19PISk1Al2fux2i6qVI3P6CG4vEgHHNxYegWyLjf2+SvLaBV1GBps5k+pWTR8omUphLgeDJpW5BVjnuU8fG8QMbGJG6bqcVzGt+CqBUzMG3TUiyfu9QAPHHiRGOqNb0o/HTess6WhTYgq8lsq2mJjIJpTVj069cP8+dMx/hZcxC5dD369x2ACT+1x4jQAOwgYNa9JLRZ21aCfDmE4sWk8qIyCPIJbw+U8XgF3gEoZfwK/0Dk+wdhNmPKDhyZg4f0x/adW5CVm4O84jKcufAbzpw9T6CLceF85T2oLOAkeq7BaJluW+Ct123fv9JrV5I/AHvpmJKyMprmM+dQduYUyi5QkysK8evZclykFm+NmI9vbrDDBmrtGVoq2DsDYXWQ6uBkQK5wczUgW0AL5MvaTFAFcg4tn2rD0whwIuUofbktyDt4zIXBnlhSg1GMfRAWv/8OFm1ciNmrqMkLKu9EI/6k6UXtACSCKats2y5vDKN4Sk2zGbLrWhmnW9VMHzcSU+cuwKJ5qzFi/FQMHvMjZt3TGjsdSAS8K8Onje7/AvkwTy6RFyyQM739UeoTgnzdtcUrgKzaw8wUnQ/w4fddMIJMvcOLz2Pz/Nk4zlgzI4fhzLmzOENTI1/4++l/AWmZZoFgvSZNtTXXtkBZYoElsf2s9fkrAW37+TKSq5M0e6XnSMLOycQXAqdKkPXLdnR7/VXM4DVoDdbJAC+c99AgDkYR42VNi2aTKKn0yAK5TC7rkjYLZEku+yTHwwvptIpJFFuQtWJiuWN1rAjzZ1Rjj7X+DbBuwhDM3rCQvngRFsxbZlKZmnmSi1V1jyYr/g1kS4N1kWrqOJXn6kvKY08cPBBzFy3B3CnLMH3hcvSa3A/LXnvW7BS32MfRaLEF8m63f4GcdgnkMpdQZLsEI5uErVwj3qMamWcNU9GoUt6X/P0Q0bsH4rZtwuGjBxgHF6DsPE12cQkuMFSRK5EGWoBIsyX6X+eq51U11RYs638LOIl1LImtdtt+zxoQ5afPGZBLzvL3zpbhVHkBfi/MwY6Z03EvAcp0CjK38j0ZzvDQ18WEUOfdg3CKJDPTy+mKIEubVQghkDVDJpAzadqvBPICtxvNpueb7Byxvn5L7Ny4CFOj52LRpDmYNmsxBg0aZDbWU32eROdvRUsWtpczXnrBctRa8KY8tkx2WzLqlQsiMGP0JCxcGYX2Q7ti3I/fYtmdd2Anfzja1QWr6C/WUEN1I8qDZIMplEx3jU5VVLqjiJ1RzOflfP8kL1Byihd7khfVtYYduj94K6bPG4d9WSk4npWHCzklwEX6P+TiLDXpN5pvyfmTp3Hu1GlcPH8BZ6jxxeWVmmwLni2AtsD9mZz/jQTl4gVDsMqKinGG5lm/dfH0WRMX5/5Gzf39Is6kFaIoqxBKHe1Ytwz9Wt+K4Tc6GzN9vaLrrWAfSMoIpiTb2x3HSV7T3V1xjEqSwNeOkJj9wohEG97NCbfDUbsa2FGzFqZ3bI8xG5di7qyFiBo3G+NmTTObnCsa0i2FtCmMABamFtBqdpaZtn1R86vKnKgc6IOvPsK0sRMwc9REzFmwEMPJJkf374gFTzyG5WR6mxzszcT2FrJEndhhBvDJlCyOzGxq85+BrAuWbKG2f0ST1/65R7F5z1YcIMPPSM/G7/SBBbmpxmwLAMWoFYxVpeHmNYF05o8+uapcD8gn8isrQhT/Ss6ePIXyUrJ3/pakICeLnUNLV3aGzPoksjJTMOiNl8204mHP2lcE80piDeqqIOew3ySqL09ydcNRF1fsJbjb6M8F8rpgB2y5wQGbG9+J6NEjMTmaCjd9DpYR6GETx5qF5/LH4eHhJv6X5ROmAvqyJuuP/rE0WvZcYt1C7um3X8TPffpg9qhJmDJpOuasiMDIYf0w5cMPMKNFG+ytVhP7HB2xnyd5iPFzDMFMImHIcvdFgWfgNUFGyE30N9XRhaZpxLdfY9uBXThWkI+8E0X4vYDgUWMrCKYAlfYqVpWZFuhnL/nsK4F3NfBtRcfRINJxrONbcbAZ9meA8iJalXMncSwrARPbf40fGctusye/CGpyRUCvJH8GsvLZAjmLkUmyqzuOOLtgN+PrHQRZBQMHGHdHkawu/+Bd7IxajLkLF2DS5OnGJ//Uv5+ZkND8sXZPFMByX2q6dqtdZteWqbYeFVBrdDR/6HZ06dwRM0dMwCTKrIjFGD1lPEZ264Gx73yKg04uOOzkjBhnZ8R7uCLGyxUJfMwg0CVuASgk+EUMMVQpWc7OOUk/ZIQXKPmdpv33sEZYbFcTX1V3wPC232HDrq04kpqKkxXnUFJRjoKyEmpROQnZBZz79aIBXNosrbPMsi3YFsAy5baAXklAQ6bBUlxaYn6r4twZlJ89bdi0BlgpXUcWY+68shysnjgM7VwcMJVaXOEfiosunjhDwK5HTlF0vRVk0pIyWj1Jvi9NNvsrnW4vlQM9kf5ZvEbhqJkPUMHeIw9g7qgfsX5lBCJnzsWEKdMxcupkk8t4+OGHzf0odLcfXbOlrFIEq10mXgLXMtnSZCUOVNrpXjcA73EUjez3swF53ORpmLpwIQYPHopB3XpjZ5260A0oYxkeJRHoBA9nxLo7Is2FwDr7XRPkfPeaOOPjhxxHX8yxuxGdPHww/JuvsHXfdiSU5KIgT8mREzhRWICi0zTZBEHadpoaKP/5v4J84RRJHQeM5oV17IqLDJdOnTRJk/wTNOW4iNwTmZjeuS06ujmbCft8L39zXYUBrlcE9EryZyCrvOm4qzPSnByRQR6T5u2HIx4kXrpnFJUn2i4I63p2xPQVkzB7zjSsnD4PE6dOQ6cBffHOW2+bUmrNN6jpmq1muWG1P4RQlharc9S0G5B3rUC0ufdudPzme4wZNgKjx1dq85BRozBgwM9Y8eSjWH9TExxg2JDCmC7V1RGJ7g4E2RUFTl7XNNe5wa44FeiLC96Mn10DsdreC718/THgwzewYfsKlGedwNkixs8kXOUEt+QUgSYIp6nFFshXAs8C+krv2UpZQZEx1zLVBmRaCaUxcZYacfoCMtNjsKhHJ3SsXh0TqcEn/UKBuvVR4strqht4RdN8Jfkzc13s54MsFyekOajilHyGXOYATfaWmvbY4OCIBc0fxI750zF13TyMGj8SS6bOxqhx4/H+d1/iyUcfM2lozf9byqqmPlGzQDfEy9Jg69ECW2nO5155CQ0aNcSrfOzZszsmT52EyTOmYOz4cebg09t/hcUPPUzTEoqUGq48YRekeHJkujkhz9njmiAXOjngbEgACtydUUqfdNo3HBHszB9usMOIVx/H+qnzkfzLAZwuqExF5pcWm+yTQBfIVwPzzwbAH6SkzIAq0yyXoGMpdCtKSEPc6q34vmlD9BTvoFVC3aY4RyDSeX6FBEKrMq4E6JXkz0AuII85TpAzqbmKSJJcPbDH3gG7SMK0lHVhx25Yv3wxpiyZRZBHY+qY8ej1Y2+89MZruKVZczPjpPs3Wr5YzQLZejQ+2fpHzbLp1igYO3Y8atWpjYcffQiff/MZZsyeghEjCe7kCRg/ciRGj/wJ0196CZv86iK1midO8OQyvVzIFl2Qx5DgmsSrphdK3VxQVNsbF2r5AyRsv/NYuuupbpD5evN7ML5LHxzZtgvpqWlIP56FghKGOpdA/l9DqJPFpcYqyEpIxLALkjMwu99w89u9bqiB3e7UXvc6QDVXlLs54HRYIBBYG3AKvyKgV5I/Zde8doGcy77KZSydSDe338HJbGaT17IVliycjUWL52LitAmYyD5XXKypYO1kHBocYsp8rKbrUbNl1mqXidefNfkmJUe0IE71X0p7KqeteVnN2/YaPQp9BnTBusceRo4TQ4Gadihxd8JJFwb19FsFHPGFNN3FBL+UhKKCxEIXfZbhleZcq3ZGVZnpcCN+dLVD57sbYMqIjtgVvxUpRVk4kVuEk1kEtJBAFRHkEj4qz1xxxvhZOm6KaraUv640xyepsaUEtIifK6jg4ymO/nK6qlO/ofR8OeKzDmL22G7ocHsd9LC3wwz7G654TrZymub1anKSmn+KGqrntt/R6xXU2l9d6iPF0R2pTtWQ7lMDB9xvQLydC461ehjRY/qZ2i2llzW3rypazZE/99xzpoBerFrJHItPySpbYtuuCbKakuBicFoS+c0335i5S22TLKDHjhiHAaMHYEyHD7CzZXMUVmdg71ADcd6OyHd2uiLIGtECWWLbYVeSfI9gxHgEYFY1e3xLzf44PAiDv/wI61YsxtGkI8jPzkNhYTHJEhn3xQsopxRSI3WXtkwSNk3ynywoRcnxfFTkFAElHAAElR/m87M4lhyHLSsiMeKrT/BV7TB8z9+YwZDusKsv8j1DrnhOtmIL6JXEAlhifV6vCeByam26gweO13RGnL8j0r0dUMYoY7d3COZ+9T42L5tnymylTNroRZvNf/jhhyYu1iY+Wv0hTmWrtWr/Fcii49pTREBr4xhtOSGglTcdPWYCJlOzf5zYByO/fQuxtZogvboL4jzs8Kvzjf8GcjnDBIGsC74ekOFGU+kSht9cwpFlH4wVds7oU6MmvqsfhLaPtsLUnt0wb+jPWDNvFvZu3YBjCUeRnaNbDgj8QiQVZJsVDqnZqUhNSUD8vl3YtnSx2a1nTKcf0PbBFvgq1Bu9CO5yO0dk1wzGRSf9pkx02BXPyVYsAK8mtp8VwNJiAVzm7IZExxuQ7euKVH8X5NrVIMiB2Pjc8xg5YzBmRi8y5lhVOiLBmkvQbFNoaKixqgK3KsBqVbX5miBbZEybxGjiQmtsPvnkE2O2tZn2j9PHY+ngiZhIsL8b9yMmvfUyMn3q4uwN1XGKZraQ8Z+k2NWVcbMbQZY2e+I0/dGVQo2qUuhDn+3kCngEAeHNgKCbkUyCt4qgRDjqRtZ2aOtYE+38vdCpQR30vL0V+j/yEAY/+wxGvPgSur/1InpTerzwOH64qyU+DvPDey7V8YlzNXzjbo8VTvbmWIl2DubYqNMS8ApGEeN/rYi80jnZiq7janKW/MP63Cle90kNdA74Mg78UipAMU10bpg7Ttk5IZ9metP9D2PVpOFYuHgBOo4ebqYRRXA1u6TpX833K2yyNmOzzLRt+4812SJlsvsjSbTcCJRy2lppoTqq/lNHY+bg8YjoOwV9x41Dh0FdsOXBR/GrfQDyavw7yGUkGAJaFyyp2mlVpTCA8WSgD/IYmqUSjMJq1QhGCBBG4uPuTRPngwRPMlL+xlpHR0TUrIm5Newxm/HszBoMQao7I8rRA+scPbHxRkdstbsBBxiPZ9MPnvUPB3w5eMJIqgKCUcLvZpLVF3i6oDSE5jrorwVZAFfo+m1ALg9xQsWN1XCaAMc0vB3ThvbAnDURiJoyHwO6DzIgKz+togD1u3ZNlG9Wkz8Wq7ZMtqXB/zHItk1JCdV/qapTZlv+efLgIfhx3EhMGzAeC/pNRq9xQzCs0xfY1/JOHHMJMKbaMtclvMAyNw8jMlkSW9N3JYGzF4HwM7F0rq8bsujrC2neTrqzg2oScBcPgP4NYurejLf9/HHWl/GrbzBO+wShxJVM1tUHF70CyIhpgkM4OAI4SFSLRd+rRXDlrtQib2dk+akonhoXGoDfA/h5j2sTw6qmuapYn7GIlmWmJXqe6WePcg68FN9GWN6jAxZEzcWY2TMwYvgYrOg/yfhhKZRy1Nadb5W+lAZbM29/CfHSgaym+UqZbK2cE8sb3rELug3vT/LFGG7AGIwaOhK9R/TB+K8/xbo2T/4byIqFBbIu+HpA/t3eA+fo4y+483kYQ5aQIOQS4DxPN5ytFYrfazjjNwetwHDHryR1v1FzJL8zZIG+z89UKD/sVJM+vRryyPyVyNC2EvlerjgTHsIwxhnHGdfrOWrVrvSbNV3IB67NGaqCeiWxJVoWuBK9lku3phuSLP/6Q0xdMAER0+dh0s+j0XfQQEwcPvTyUlTlqLVWTeue1KyJCAGskMkC9r8CWQfQgSRqMg+a0lIBt4D+5ptPTFlQj/FD8MP4oRjbf6ipuvxp1ACM+6HTH0A25IsgS3SBkit1nK1oUv1CcBi1ORTnb3TFBZo1+eff/ELMTgKng8JQRlOrtVOqtFAlSr6XO0q8qTleXjhPsC4ytPvVwwfnGbJpywlVThb5BaA4IMgcQ8cCGe3F6jThjIXPU+srGKdm+1/5nGzlSqDaSlWiZYFsDfLTJHvrvn4FE1ePxpSZkzGj3xhE9h2HcQMH4sXBX5k5fS140P0lxLKlcNJai3D9JcTLAlfNSoEqASFKrxuSPPnkk8ZfqFpQ89AKq2RitGOBHmd17o7drR9HQU1/susaOB5mh0wy7yIHe1zwoJaRPF3gczi44Dde+CmCU+pHf0057/fHQXClTrR9/++Qsy48DxcB6W22sdCeJWbfEhUJkDP8bheCUy5BOE4fX+jvjDIPB4aFdA3kCCfsPDGmV3+sXhCJCWPGYujIEeg/cih+HjMC/YYNxnuffGQqMBs1aoROnTqZAkTbVlVj/6xdE2QLWDUrG6Yms/35559X3luZI030XvtkK7QS3RdhENCdh/fCzG+/RGar+4AbqE0kTyne1ZAVQI10pBmjKT1LfypzrA1ctNyziB10kqbyV6c/94n/GJDd+MjzNhUgBFo7EZU5y0pVAl8eFI7C6k7GVeT4O+FUNUf64Bo4FlQfqz55ESvnLsKU0dRcgvxjv74YM3USuvTqgU+//AJvvfO2Kd5Q32r/TEuDLfN8JS2+UrsmyLYH0o9YTWZb63pF55URk+lW/KzCboVWytJI2/sM7YdeY37EhI5fIuaWB/iLofjNzhXlZMI57tVRQP+sbZrKSLCKXMiiPWlGPYNxwZXM16HOv3fqpSTK9SZT/q9F4J718sc5TxI+An6SrqGCzP0Mr+eCux8Sg+2RHe6Gc95u+JVhmkKlmPDGWPv1x1iycDQiFixEvz59MXjoEIwaNxaffPE5+vbvh6effApNGt1kZpgUvlrNIli2POla7bqIl2UWBLitZusHtSpB6TUV/olxqyhfYCtLpo3fpvYdhD79fsK3I3qjz4DO2PL0K/ST9XCGzDbLwQ4nSZrO2LuaNUTyU0X0l+X0kRc9CLJTrT+EKwpHqort+3+HqH5LQGuHIW1Ac8HVRghymp8dOQBjcPIJ2PkjuWUbRPzUHlPmj8OCKVMwhPylc/dumDBlMn7o0N6sQ3vmqafxYJt7cWuzFqZ/rT63BVbE63rbdYFsy96qMjm1VatWGY1WfttaYiMTo2T6YLLE0V36Y0jvQeg8cRR60HwveOUFpATVJYlyw28c/aB/gqMrnzvjNMOjMn8yUcaV2k7CijetTrUF+BwHhG2H/x0iIidfLL98npbodzd/A67ZN8yeUQFDszK6qHxq8J4nHseKCQMwf/5UjB87Dn1/HorOfXph8pyZ+OyrLw2vee2VV3FzvQa4rcUtWLN8pelf9bPyFf8JsLbtukAW+bI12/pRiUy2Rcy0EboX2ayqPEXGxAplsvv07Ib+Q4Zg8MDhGNxzIF/rg0FDu2PRp2/hYJPbGR8GIYf+q/xGe5y58Qaau2ooJTkp8nRGkeqWyUAlYqm2PvifYq5FsOR7NSC1w5C2kpJPznF0Rrq9AzmIK3JrN8a2997A7BF0WzPGEuAJGPbzCPTu9zP6DBuETj26oV37H/DSCy/ilibNEOzrj2WRS9jRpmv/oMHqc4sb2brPq7XrAlnmoqr2qtkG5XquGE51YYqjFbxrF/Ufvv8CHQb3Qu/hgzC8808Y1/FHjBg6EMOG9Mbw9t8g+p77sL1RMyS6+yPvBkeUVqth1hApz11CMmaFG1bIURXsv1uKnHV+XoZln3Kku2HcLoC1wavuKhfT6A6s690eM1ZNx9jJ4zGi+0D07zMEPw0Zin4//oT2PbrizXffwfPPPofnnngKzRrdjOmTp5j+FYSaPrTVYGHxfwKy7cFsfbKl3dZr+nHtKyUSplu9KlnyQJe30fm7LzG9fQ+M/nkg2XY/dOjbCz/1H4Cfx45FFH345jffR1zzu5FHP1xegyawhhfKHMi0HQOuGFtaQP8TwM6t4WhAvujhb7Q4v7ojjmtSpnYoTrVuiulLRmLcpMH4SRo7chT6DyKT7jIUA37ojk+7fov3P/0YL7z0Ilo2bYZm9Rsheukyo8EqFT7127/62lIoq10vs1a7LpCv1RQ3256AigAV3yn9KdOtmROFV8reiJgp5y1Trv+/ndEDfUZ1w/SO32Pt86/hQOPbzBqqMrcb8Svj6Ysujmajl5POzmZvEbHxfE9tRaGVGaFmA5YTAV6mVkrF6xX0h6fcAi5JEMp9Qq8qyqCdDuJn/Wh2vTxw2t0dF9y0kNwD8PDEOQ/yBlc3wIW8gYxZGltM/6vSpjwHWhtvO5yg5LjeiBM3UIMdaiOh9ZNY17MHZq+aYXIF4ia6Xk3qSJSL1jJhFcWrf7S8RYohBbGaqmD+qvY/g2xVkKhpkbhlSnbu3GkqGLQIS/XbuhiFVyIXYt+aWdHFjvy2HYbytYHDB2DwyD6Y1JUm/PnHcbROPZyo6UYGXhN5To7GhGufrt8dCYCDM36nBl2s4YCzNV1x3sETvztpWtIf8A7F7/61cTGoDs4F1wFcGY9fTaoFADX8eEx/nHcOIMiBqPAOQIlfoMmIlRPkPE8XZHs6oNDFHucc7A1BPBXkgbz6niit6UgC6YQLNwYh6c4HsKDP9xgQQVM8YzhG9elvSKhyBxrcui+Etnz47LPPzJywVjwIYPWTqi3lb9WsPvyr2v8MsnViavIftieoJZSaHlOsp4tSpb/uJyigpdlKlvTp3BO9evZBz4EMtYaNwNDBQzCJGj7n88+w6I3XkRd+E7W0NkocqKWMM0+SqZ6sbkdGa4eygBtMWlJ7aKoS5ZyzNy7QL553YJzKsEz5Z2XPrianvMIuy0nPUB43BKWugShy8kOhoy+yPMkPPN1MBq6glhdS6rohPaAGQ0CeB6Wgzn345bHnMaPLlxg/qR9mzhyPWeMmYdCAYWjbu5/JVEkEtnY90O48mgvWjJJSwxrwKoq3mlXCo3a9Pvda7S8x1wLW1kfI1FhTlBoE2sZBO9JoSaw0WrXCCrE0sr8c3Q9t+/+I7p16ov83vTGkbV/83Gsgeo8Yik6TGWIM+BRjv3wD2+99CMU+N/GMffCbkgo1bkQ+gTY1U5emLc/S1J5yd8FJDy3+dkKZNx+dqI1XkTPUxDP2TsYdaBpUpcNmbw8/L2T7e+FXf20N5YaC6vY4ZVcD5+xccNYpDPG33oKVL9+LEQN7YMyUoRizcBKGTxmNAT1/RJ+vOmPQd70xqsewywki3eXlpZdeMuDqZtbKLUjDbeujRWBtmbTVh/9r+0tA1ogTmFXjOIuQaXRqfw5Nk+nuY/I/iqdVbdL+m8/wQ7tv0LZrB3zfqzu+79kL7bv3QreuvdG7S29M7fUzRtKf9R/UHf36fI0Jn7+MTffdiVzvOvjVjmEUQ5TTds7UqpqMR2ugpHo1FLvWQJGPPQoDORi8Kovo/kwq/KtzQFRDhQsHTDU7HsuOx/yXJNFEV5hERjDOed+CI/e/gIVtv8ek8cMwceF0dJ47AsNJqmZ06IUx33VC+y4d8OFPHfBV7y7o0aWr2a9E16lZJA1y5fu1JaL2qLYlrOo/28jF9vn/2v5nkK2TkSZbs1W2J2jF0XpfBE15bY1k1Qtr6uz9hx/C+6+8gA+//Rif9GqHb/p2RZce3THoh24Y+3VXDO74M4Z0HYihPw3CiP79MKpfL4zq+QNGd/wco7/7APvvvheHWt2O+PpNkeZTC3k1GXYR9LME/KxdNTJ1mtWryFlHR5xyoMZTm4spRU5uJnQr8w7iAAhHbLNbsPGFxzCn5xeYOmUAZs2cgmkTZmHwkGno1H8iJrftgb7tO6Bttx/QuVdnDOzN/7t2xqfff4ZHP3vZrP5XbZwSRdJeWTVrs3E1KUbVPMRf3f4STbY9QY1KzZbYmp2CgoLL/kUXpVysCtJUjNb6zjtwz3334sknHsPbr72M7z77BF07tEPv7t3wY++e+LxnN3zRvSu+69IZnTp3xY/de2NwvwEYN3QEJo4ei3EkbNP6/4RFnTpiw0dfYN9zbyLx7qeR0fwBZDe822xHfDUptL8ZuZ7NkVT3Lg6Yx7Hh9TexrN0XmD+wI2aP6omfIkdiyGySqAnDMGjkIHQbPBDtf+qDbh1+RL8veqHj9+3Rgb/drmdXfP9jN3zFKOH9997CS+Qgj7VqbQayJhl0ez1pr9UP6idb/yvFkGbb+uF/lE9WE9BVT0pAW0RMF6H/rQGhIjvtd9G46Z24qUkrNG9xK+69/z48T61++5P38f5Xn+C9rwl4p+/Rg6D3ov/u9UMndGvfDT+QrH3drTc+7fkTvu7+M37o/TO6DxiKvsM0TTcEAycyRJvcF32m9MK0wcOvKqMmDcfoySMwesIojB0/BmNHj8PYEeMxZsh4jBo0HkO6DcNP3Rnjdv8J33fthnadfkDHTm0ZAn2HLt2+RbsevfB12w745OMv8fZb7+O5l19Dm0cfQYMWzeEXHmYmb1ROa7kymWXbXIP67K80zVdqfxnIf9Z0AVcTlRQJbPlrbWPRsGFDM3WpRIpEGi+CZjFylRyJtFmsVavs9b8e9b5CFcXfVkyu+FRFcJosseJUPddrek8MX6xXYj3XdzVRINIk0f86vrJ42k1YREqPmmrV1sPK7OlcBahckfID0lxdl+XCJAJXg14icP8qTb1W+9tBVpN2a+sllZ+Kfcq8aUWl1kgr/FL5y+uvv24A15JadbgAFhDqcD3Xo0AWKHrUewJZA8BW9F1LrMGjxIS+o9esz1jHEcACX8eyBpbiXIVBCgc1+6Y0rhaBi2foddVKa5spXVdVUPWaxPb6/6/b3w6yZb6tpoSKdtVTfK1EgaoidD8FdaayaNIQdaSmNVUabGm2QBFo0jBpl16zQJdYg6GqaDDoUZ/V9yXW5/WefkPhj7J2EoVBmoQRsNJcFU2IVIlBazss3Tpfq04sTmJd59/Z/s9Bvp4moNUx1mpKNe3Cp83WlBLUuh+Vorq6upptBZVU0bJadbLMvMIxLcLWzJe0TBkkAa31uxoEei7wLW22RO+rdMl6X5/V95Vu1PG0mEwWRMfXb+lupgJVJlkDUKRKS4Z0N7wrpSGrhpR/V/vbQRYTr6rNtsREpk77amqLC3WoOl5mUasIBLg0XGZdd8ORaRf4GhTSeIlAEmASZZvkP21Fmqljav8rgSlroUGkVKx163hZE4nmzKXNqnjR+cjq2DaBapvmvVr7f6nh/whNVhOY6iSLnOi5lfFRZ+g1PYqdJiQkmK0GFXNrBzrlx7WER2lCkTf9r8SDRHGqBoJA02cleq7XJNJGxa8yuZo5U2ZOu8yqxlkAaxCIpMmFKP1oZag0MHWO8rMC2xZcabVtJkvNAlWi71ry/6L97SDrom1JidWqvq5HDYArdYw6WXdp04apAl4+VT5dmmxpuUytgJTIx+t/+VVpvsXi5YNVnyZAtf+3kha2JtfWwljNFkydmwah1a50rv8/CfK1mpUxU5M2VzXlf9ZRlumUCAjF5QrXtN+mHpWgsXbftTTSGkg6rtUs7dN7VtNnbBMZAtb2f71vC3bVZh3z/w/yX9RstcbqXNtmvfZn8v/9Dfj/ACeXwqpF6dcqAAAAAElFTkSuQmCC - - - True - 4fkGUxKnlUae43TI8NsnLQ== - 2016-09-10T16:29:17Z - V/rJOkFjYUqrFCDdP8HeSw== - 2016-09-10T16:36:55Z - 10 - 6291456 - SkjqCvDUmU6y6k7onr6ewg== - oy863aK0cEKApjbJfGLHZQ== - - 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 - - - - - - oy863aK0cEKApjbJfGLHZQ== - ExampleDatabase - - 49 - - 2016-09-10T16:29:17Z - 2016-09-10T16:29:17Z - 2016-09-10T18:50:45Z - 2016-09-10T15:04:19Z - False - 19 - 2016-09-10T16:29:17Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - 9truGTw7i0+svm1EKjSCXQ== - 0 - - - - - - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T15:04:19Z - False - 0 - 2016-09-10T16:29:32Z - - - Notes - Notes - - - Password - Password - - - Title - Sample Entry - - - URL - http://keepass.info/ - - - UserName - User Name - - - True - 0 - - Target Window - {USERNAME}{TAB}{PASSWORD}{TAB}{ENTER} - - - - - - qgRU1hFDE0az+mXL5mTrFg== - 0 - - - - - - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T15:04:19Z - False - 0 - 2016-09-10T16:29:32Z - - - Password - 12345 - - - Title - Sample Entry #2 - - - URL - http://keepass.info/help/kb/testform.html - - - UserName - Michael321 - - - True - 0 - - *Test Form - KeePass* - - - - - - - y5JBCSQMd0qKhGu3M9XjIw== - General - - 48 - - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T18:50:41Z - 2016-09-10T15:04:19Z - False - 2 - 2016-09-10T16:29:32Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - 0w2ac9Zj6Uqp6Up46CyNhQ== - Windows - - 38 - - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T18:50:36Z - 2016-09-10T15:04:19Z - False - 2 - 2016-09-10T16:29:32Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - zlA7tzCMp0WLJD8Bjw+rCw== - Network - - 3 - - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T18:50:36Z - 2016-09-10T15:04:19Z - False - 1 - 2016-09-10T16:29:32Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - JATpnerLXU+0DtRco5cx6Q== - Internet - - 1 - - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T18:50:37Z - 2016-09-10T15:04:19Z - False - 2 - 2016-09-10T16:29:32Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - KS6v+Pw92EmIby9paykzpQ== - eMail - - 19 - - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T18:50:38Z - 2016-09-10T15:04:19Z - False - 1 - 2016-09-10T16:29:32Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - 4fkGUxKnlUae43TI8NsnLQ== - Recycle Bin - - 43 - - 2016-09-10T16:30:02Z - 2016-09-10T16:30:02Z - 2016-09-10T18:50:45Z - 2016-09-10T15:04:19Z - False - 7 - 2016-09-10T16:30:02Z - - True - - false - false - AAAAAAAAAAAAAAAAAAAAAA== - - SkjqCvDUmU6y6k7onr6ewg== - Homebanking - - 37 - - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T18:50:45Z - 2016-09-10T15:04:19Z - False - 8 - 2016-09-10T16:30:02Z - - True - - null - null - qYwf0cP0sU6mfMsfj+s9EA== - - qYwf0cP0sU6mfMsfj+s9EA== - 37 - kJh5KNGNaU6SZIsfsAwx7g== - - - - c;b;a - - 2016-09-10T16:29:50Z - 2016-09-11T10:44:40Z - 2016-09-11T10:44:40Z - 2016-09-10T15:04:19Z - False - 9 - 2016-09-10T16:29:50Z - - - Notes - - - - Password - 1abEUk2xWlLtfDIvzwYJ - - - Title - BinaryField - - - URL - - - - UserName - - - - letter J.jpeg - - - - True - 0 - - - - qYwf0cP0sU6mfMsfj+s9EA== - 37 - - - - - - 2016-09-10T16:29:50Z - 2016-09-10T16:29:56Z - 2016-09-10T16:29:56Z - 2016-09-10T15:04:19Z - False - 1 - 2016-09-10T16:29:50Z - - - Notes - - - - Password - 1abEUk2xWlLtfDIvzwYJ - - - Title - hhs - - - URL - - - - UserName - - - - True - 0 - - - - qYwf0cP0sU6mfMsfj+s9EA== - 37 - kJh5KNGNaU6SZIsfsAwx7g== - - - - - - 2016-09-10T16:29:50Z - 2016-09-10T17:26:56Z - 2016-09-10T17:45:44Z - 2016-09-10T15:04:19Z - False - 3 - 2016-09-10T16:29:50Z - - - Notes - - - - Password - 1abEUk2xWlLtfDIvzwYJ - - - Title - hhs - - - URL - - - - UserName - - - - True - 0 - - - - qYwf0cP0sU6mfMsfj+s9EA== - 37 - kJh5KNGNaU6SZIsfsAwx7g== - - - - - - 2016-09-10T16:29:50Z - 2016-09-10T18:39:40Z - 2016-09-10T18:40:02Z - 2016-09-10T15:04:19Z - False - 6 - 2016-09-10T16:29:50Z - - - Notes - - - - Password - 1abEUk2xWlLtfDIvzwYJ - - - Title - hhs - - - URL - - - - UserName - - - - letter J.jpeg - - - - True - 0 - - - - qYwf0cP0sU6mfMsfj+s9EA== - 37 - kJh5KNGNaU6SZIsfsAwx7g== - - - - a b c - - 2016-09-10T16:29:50Z - 2016-09-10T18:50:20Z - 2016-09-10T18:50:20Z - 2016-09-10T15:04:19Z - False - 7 - 2016-09-10T16:29:50Z - - - Notes - - - - Password - 1abEUk2xWlLtfDIvzwYJ - - - Title - hhs - - - URL - - - - UserName - - - - letter J.jpeg - - - - True - 0 - - - - qYwf0cP0sU6mfMsfj+s9EA== - 37 - kJh5KNGNaU6SZIsfsAwx7g== - - - - c;b;a - - 2016-09-10T16:29:50Z - 2016-09-10T18:51:05Z - 2016-09-10T18:51:05Z - 2016-09-10T15:04:19Z - False - 8 - 2016-09-10T16:29:50Z - - - Notes - - - - Password - 1abEUk2xWlLtfDIvzwYJ - - - Title - hhs - - - URL - - - - UserName - - - - letter J.jpeg - - - - True - 0 - - - - - - - - V/rJOkFjYUqrFCDdP8HeSw== - Templates - - 48 - - 2016-09-10T16:36:24Z - 2016-09-10T16:36:29Z - 2016-09-10T18:50:40Z - 2016-09-09T23:00:00Z - False - 4 - 2016-09-10T16:36:32Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - - + + + + KeePass + + 2016-09-10T16:29:17Z + + 2016-09-10T16:29:17Z + + 2016-09-10T16:29:17Z + 365 + #FF0000 + 2016-09-10T16:29:17Z + -1 + -1 + + False + False + True + False + False + + + + kJh5KNGNaU6SZIsfsAwx7g== + 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 + + + True + 4fkGUxKnlUae43TI8NsnLQ== + 2016-09-10T16:29:17Z + V/rJOkFjYUqrFCDdP8HeSw== + 2016-09-10T16:36:55Z + 10 + 6291456 + SkjqCvDUmU6y6k7onr6ewg== + oy863aK0cEKApjbJfGLHZQ== + + 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 + + + + + + oy863aK0cEKApjbJfGLHZQ== + ExampleDatabase + + 49 + + 2016-09-10T16:29:17Z + 2016-09-10T16:29:17Z + 2016-09-10T18:50:45Z + 2016-09-10T15:04:19Z + False + 19 + 2016-09-10T16:29:17Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + 9truGTw7i0+svm1EKjSCXQ== + 0 + + + + + + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T15:04:19Z + False + 0 + 2016-09-10T16:29:32Z + + + Notes + Notes + + + Password + Password + + + Title + Sample Entry + + + URL + http://keepass.info/ + + + UserName + User Name + + + True + 0 + + Target Window + {USERNAME}{TAB}{PASSWORD}{TAB}{ENTER} + + + + + + qgRU1hFDE0az+mXL5mTrFg== + 0 + + + + + + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T15:04:19Z + False + 0 + 2016-09-10T16:29:32Z + + + Password + 12345 + + + Title + Sample Entry #2 + + + URL + http://keepass.info/help/kb/testform.html + + + UserName + Michael321 + + + True + 0 + + *Test Form - KeePass* + + + + + + + y5JBCSQMd0qKhGu3M9XjIw== + General + + 48 + + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T18:50:41Z + 2016-09-10T15:04:19Z + False + 2 + 2016-09-10T16:29:32Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + 0w2ac9Zj6Uqp6Up46CyNhQ== + Windows + + 38 + + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T18:50:36Z + 2016-09-10T15:04:19Z + False + 2 + 2016-09-10T16:29:32Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + zlA7tzCMp0WLJD8Bjw+rCw== + Network + + 3 + + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T18:50:36Z + 2016-09-10T15:04:19Z + False + 1 + 2016-09-10T16:29:32Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + JATpnerLXU+0DtRco5cx6Q== + Internet + + 1 + + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T18:50:37Z + 2016-09-10T15:04:19Z + False + 2 + 2016-09-10T16:29:32Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + KS6v+Pw92EmIby9paykzpQ== + eMail + + 19 + + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T18:50:38Z + 2016-09-10T15:04:19Z + False + 1 + 2016-09-10T16:29:32Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + 4fkGUxKnlUae43TI8NsnLQ== + Recycle Bin + + 43 + + 2016-09-10T16:30:02Z + 2016-09-10T16:30:02Z + 2016-09-10T18:50:45Z + 2016-09-10T15:04:19Z + False + 7 + 2016-09-10T16:30:02Z + + True + + false + false + AAAAAAAAAAAAAAAAAAAAAA== + + SkjqCvDUmU6y6k7onr6ewg== + Homebanking + + 37 + + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T18:50:45Z + 2016-09-10T15:04:19Z + False + 8 + 2016-09-10T16:30:02Z + + True + + null + null + qYwf0cP0sU6mfMsfj+s9EA== + + qYwf0cP0sU6mfMsfj+s9EA== + 37 + kJh5KNGNaU6SZIsfsAwx7g== + + + + c;b;a + + 2016-09-10T16:29:50Z + 2016-09-11T10:44:40Z + 2016-09-11T10:44:40Z + 2016-09-10T15:04:19Z + False + 9 + 2016-09-10T16:29:50Z + + + Notes + + + + Password + 1abEUk2xWlLtfDIvzwYJ + + + Title + BinaryField + + + URL + + + + UserName + + + + letter J.jpeg + + + + True + 0 + + + + qYwf0cP0sU6mfMsfj+s9EA== + 37 + + + + + + 2016-09-10T16:29:50Z + 2016-09-10T16:29:56Z + 2016-09-10T16:29:56Z + 2016-09-10T15:04:19Z + False + 1 + 2016-09-10T16:29:50Z + + + Notes + + + + Password + 1abEUk2xWlLtfDIvzwYJ + + + Title + hhs + + + URL + + + + UserName + + + + True + 0 + + + + qYwf0cP0sU6mfMsfj+s9EA== + 37 + kJh5KNGNaU6SZIsfsAwx7g== + + + + + + 2016-09-10T16:29:50Z + 2016-09-10T17:26:56Z + 2016-09-10T17:45:44Z + 2016-09-10T15:04:19Z + False + 3 + 2016-09-10T16:29:50Z + + + Notes + + + + Password + 1abEUk2xWlLtfDIvzwYJ + + + Title + hhs + + + URL + + + + UserName + + + + True + 0 + + + + qYwf0cP0sU6mfMsfj+s9EA== + 37 + kJh5KNGNaU6SZIsfsAwx7g== + + + + + + 2016-09-10T16:29:50Z + 2016-09-10T18:39:40Z + 2016-09-10T18:40:02Z + 2016-09-10T15:04:19Z + False + 6 + 2016-09-10T16:29:50Z + + + Notes + + + + Password + 1abEUk2xWlLtfDIvzwYJ + + + Title + hhs + + + URL + + + + UserName + + + + letter J.jpeg + + + + True + 0 + + + + qYwf0cP0sU6mfMsfj+s9EA== + 37 + kJh5KNGNaU6SZIsfsAwx7g== + + + + a b c + + 2016-09-10T16:29:50Z + 2016-09-10T18:50:20Z + 2016-09-10T18:50:20Z + 2016-09-10T15:04:19Z + False + 7 + 2016-09-10T16:29:50Z + + + Notes + + + + Password + 1abEUk2xWlLtfDIvzwYJ + + + Title + hhs + + + URL + + + + UserName + + + + letter J.jpeg + + + + True + 0 + + + + qYwf0cP0sU6mfMsfj+s9EA== + 37 + kJh5KNGNaU6SZIsfsAwx7g== + + + + c;b;a + + 2016-09-10T16:29:50Z + 2016-09-10T18:51:05Z + 2016-09-10T18:51:05Z + 2016-09-10T15:04:19Z + False + 8 + 2016-09-10T16:29:50Z + + + Notes + + + + Password + 1abEUk2xWlLtfDIvzwYJ + + + Title + hhs + + + URL + + + + UserName + + + + letter J.jpeg + + + + True + 0 + + + + + + + + V/rJOkFjYUqrFCDdP8HeSw== + Templates + + 48 + + 2016-09-10T16:36:24Z + 2016-09-10T16:36:29Z + 2016-09-10T18:50:40Z + 2016-09-09T23:00:00Z + False + 4 + 2016-09-10T16:36:32Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + + \ No newline at end of file diff --git a/test/src/main/resources/KeyFileDatabase.kdbx b/test/src/test/resources/KeyFileDatabase.kdbx similarity index 100% rename from test/src/main/resources/KeyFileDatabase.kdbx rename to test/src/test/resources/KeyFileDatabase.kdbx diff --git a/test/src/main/resources/KeyFileDatabase.key b/test/src/test/resources/KeyFileDatabase.key similarity index 95% rename from test/src/main/resources/KeyFileDatabase.key rename to test/src/test/resources/KeyFileDatabase.key index 6f63430b..aade8cbf 100755 --- a/test/src/main/resources/KeyFileDatabase.key +++ b/test/src/test/resources/KeyFileDatabase.key @@ -1,9 +1,9 @@ - - - - 1.00 - - - uE9L7SGiT0+PrSvZ0oIzaLjtpsiu50ciR/JXc44Pw24= - - + + + + 1.00 + + + uE9L7SGiT0+PrSvZ0oIzaLjtpsiu50ciR/JXc44Pw24= + + diff --git a/test/src/main/resources/NoPasswordWithKey.kdbx b/test/src/test/resources/NoPasswordWithKey.kdbx similarity index 100% rename from test/src/main/resources/NoPasswordWithKey.kdbx rename to test/src/test/resources/NoPasswordWithKey.kdbx diff --git a/test/src/main/resources/NoPasswordWithKey.key b/test/src/test/resources/NoPasswordWithKey.key similarity index 95% rename from test/src/main/resources/NoPasswordWithKey.key rename to test/src/test/resources/NoPasswordWithKey.key index 6fdee6b8..300da4d5 100755 --- a/test/src/main/resources/NoPasswordWithKey.key +++ b/test/src/test/resources/NoPasswordWithKey.key @@ -1,9 +1,9 @@ - - - - 1.00 - - - RJS69T/8tKYwO/fXPNGdtHlgLM5VfJmBDQLOlsg1AcE= - - + + + + 1.00 + + + RJS69T/8tKYwO/fXPNGdtHlgLM5VfJmBDQLOlsg1AcE= + + diff --git a/test/src/main/resources/V3-CustomIcon.kdbx b/test/src/test/resources/V3-CustomIcon.kdbx similarity index 100% rename from test/src/main/resources/V3-CustomIcon.kdbx rename to test/src/test/resources/V3-CustomIcon.kdbx diff --git a/test/src/main/resources/V4-AES-AES.kdbx b/test/src/test/resources/V4-AES-AES.kdbx similarity index 100% rename from test/src/main/resources/V4-AES-AES.kdbx rename to test/src/test/resources/V4-AES-AES.kdbx diff --git a/test/src/main/resources/V4-AES-Argon2-CustomIcon.kdbx b/test/src/test/resources/V4-AES-Argon2-CustomIcon.kdbx similarity index 100% rename from test/src/main/resources/V4-AES-Argon2-CustomIcon.kdbx rename to test/src/test/resources/V4-AES-Argon2-CustomIcon.kdbx diff --git a/test/src/main/resources/V4-AES-Argon2.kdbx b/test/src/test/resources/V4-AES-Argon2.kdbx similarity index 100% rename from test/src/main/resources/V4-AES-Argon2.kdbx rename to test/src/test/resources/V4-AES-Argon2.kdbx diff --git a/test/src/main/resources/V4-ChaCha20-AES.kdbx b/test/src/test/resources/V4-ChaCha20-AES.kdbx similarity index 100% rename from test/src/main/resources/V4-ChaCha20-AES.kdbx rename to test/src/test/resources/V4-ChaCha20-AES.kdbx diff --git a/test/src/main/resources/V4-ChaCha20-Argon2-Attachment.kdbx b/test/src/test/resources/V4-ChaCha20-Argon2-Attachment.kdbx similarity index 100% rename from test/src/main/resources/V4-ChaCha20-Argon2-Attachment.kdbx rename to test/src/test/resources/V4-ChaCha20-Argon2-Attachment.kdbx diff --git a/test/src/main/resources/V4-CustomIcon.kdbx b/test/src/test/resources/V4-CustomIcon.kdbx similarity index 100% rename from test/src/main/resources/V4-CustomIcon.kdbx rename to test/src/test/resources/V4-CustomIcon.kdbx diff --git a/test/src/main/resources/issue-27/bogus-timestamp.kdbx b/test/src/test/resources/issue-27/bogus-timestamp.kdbx similarity index 100% rename from test/src/main/resources/issue-27/bogus-timestamp.kdbx rename to test/src/test/resources/issue-27/bogus-timestamp.kdbx diff --git a/test/src/main/resources/issue-27/bogus-timestamp2.kdbx b/test/src/test/resources/issue-27/bogus-timestamp2.kdbx similarity index 100% rename from test/src/main/resources/issue-27/bogus-timestamp2.kdbx rename to test/src/test/resources/issue-27/bogus-timestamp2.kdbx diff --git a/test/src/main/resources/issue-38/Database/Database.kdbx b/test/src/test/resources/issue-38/Database/Database.kdbx similarity index 100% rename from test/src/main/resources/issue-38/Database/Database.kdbx rename to test/src/test/resources/issue-38/Database/Database.kdbx diff --git a/test/src/main/resources/issue-38/Database/Database.keyx b/test/src/test/resources/issue-38/Database/Database.keyx similarity index 100% rename from test/src/main/resources/issue-38/Database/Database.keyx rename to test/src/test/resources/issue-38/Database/Database.keyx diff --git a/test/src/main/resources/issue-38/Database/README.md b/test/src/test/resources/issue-38/Database/README.md similarity index 100% rename from test/src/main/resources/issue-38/Database/README.md rename to test/src/test/resources/issue-38/Database/README.md diff --git a/test/src/main/resources/kdb.key b/test/src/test/resources/kdb.key similarity index 100% rename from test/src/main/resources/kdb.key rename to test/src/test/resources/kdb.key diff --git a/test/src/main/resources/kdb_with_random_file.kdbx b/test/src/test/resources/kdb_with_random_file.kdbx similarity index 100% rename from test/src/main/resources/kdb_with_random_file.kdbx rename to test/src/test/resources/kdb_with_random_file.kdbx diff --git a/test/src/main/resources/kdbwithkey.kdb b/test/src/test/resources/kdbwithkey.kdb similarity index 100% rename from test/src/main/resources/kdbwithkey.kdb rename to test/src/test/resources/kdbwithkey.kdb diff --git a/test/src/main/resources/kdbxSample.xml b/test/src/test/resources/kdbxSample.xml similarity index 100% rename from test/src/main/resources/kdbxSample.xml rename to test/src/test/resources/kdbxSample.xml diff --git a/test/src/main/resources/kdbx_hash_test.kdbx b/test/src/test/resources/kdbx_hash_test.kdbx similarity index 100% rename from test/src/main/resources/kdbx_hash_test.kdbx rename to test/src/test/resources/kdbx_hash_test.kdbx diff --git a/test/src/main/resources/kdbx_hash_test.keyx b/test/src/test/resources/kdbx_hash_test.keyx similarity index 95% rename from test/src/main/resources/kdbx_hash_test.keyx rename to test/src/test/resources/kdbx_hash_test.keyx index 31d047f3..e5254af2 100755 --- a/test/src/main/resources/kdbx_hash_test.keyx +++ b/test/src/test/resources/kdbx_hash_test.keyx @@ -1,12 +1,12 @@ - - - - 2.0 - - - - 66F110CA E32995B5 EFA8E672 1E70C773 - 48F8E260 EEDD8744 93F41803 5BFDC27D - - + + + + 2.0 + + + + 66F110CA E32995B5 EFA8E672 1E70C773 + 48F8E260 EEDD8744 93F41803 5BFDC27D + + \ No newline at end of file diff --git a/test/src/main/resources/kdbx_hash_test_wrong_hash.keyx b/test/src/test/resources/kdbx_hash_test_wrong_hash.keyx similarity index 95% rename from test/src/main/resources/kdbx_hash_test_wrong_hash.keyx rename to test/src/test/resources/kdbx_hash_test_wrong_hash.keyx index 5c7b37e2..2bfa7c09 100755 --- a/test/src/main/resources/kdbx_hash_test_wrong_hash.keyx +++ b/test/src/test/resources/kdbx_hash_test_wrong_hash.keyx @@ -1,12 +1,12 @@ - - - - 2.0 - - - - 66F110CA E32995B5 EFA8E672 1E70C773 - 48F8E260 EEDD8744 93F41803 5BFDC27D - - + + + + 2.0 + + + + 66F110CA E32995B5 EFA8E672 1E70C773 + 48F8E260 EEDD8744 93F41803 5BFDC27D + + \ No newline at end of file diff --git a/test/src/main/resources/kdbx_keyfile32.kdbx b/test/src/test/resources/kdbx_keyfile32.kdbx similarity index 100% rename from test/src/main/resources/kdbx_keyfile32.kdbx rename to test/src/test/resources/kdbx_keyfile32.kdbx diff --git a/test/src/main/resources/kdbx_keyfile64.kdbx b/test/src/test/resources/kdbx_keyfile64.kdbx similarity index 100% rename from test/src/main/resources/kdbx_keyfile64.kdbx rename to test/src/test/resources/kdbx_keyfile64.kdbx diff --git a/test/src/main/resources/keyfile32 b/test/src/test/resources/keyfile32 similarity index 100% rename from test/src/main/resources/keyfile32 rename to test/src/test/resources/keyfile32 diff --git a/test/src/main/resources/keyfile64 b/test/src/test/resources/keyfile64 similarity index 100% rename from test/src/main/resources/keyfile64 rename to test/src/test/resources/keyfile64 diff --git a/test/src/main/resources/letter J.jpeg b/test/src/test/resources/letter J.jpeg similarity index 100% rename from test/src/main/resources/letter J.jpeg rename to test/src/test/resources/letter J.jpeg diff --git a/test/src/main/resources/letter L.jpeg b/test/src/test/resources/letter L.jpeg similarity index 100% rename from test/src/main/resources/letter L.jpeg rename to test/src/test/resources/letter L.jpeg diff --git a/test/src/main/resources/random_file b/test/src/test/resources/random_file similarity index 100% rename from test/src/main/resources/random_file rename to test/src/test/resources/random_file diff --git a/test/src/main/resources/test1.kdbx b/test/src/test/resources/test1.kdbx similarity index 100% rename from test/src/main/resources/test1.kdbx rename to test/src/test/resources/test1.kdbx diff --git a/test/src/main/resources/test123-AES-AES.kdbx b/test/src/test/resources/test123-AES-AES.kdbx similarity index 100% rename from test/src/main/resources/test123-AES-AES.kdbx rename to test/src/test/resources/test123-AES-AES.kdbx diff --git a/test/src/main/resources/test123-AES-Argon2.kdbx b/test/src/test/resources/test123-AES-Argon2.kdbx similarity index 100% rename from test/src/main/resources/test123-AES-Argon2.kdbx rename to test/src/test/resources/test123-AES-Argon2.kdbx diff --git a/test/src/main/resources/test123-ChaCha20-AES.kdbx b/test/src/test/resources/test123-ChaCha20-AES.kdbx similarity index 100% rename from test/src/main/resources/test123-ChaCha20-AES.kdbx rename to test/src/test/resources/test123-ChaCha20-AES.kdbx diff --git a/test/src/main/resources/test123-ChaCha20-Argon2.kdbx b/test/src/test/resources/test123-ChaCha20-Argon2.kdbx similarity index 100% rename from test/src/main/resources/test123-ChaCha20-Argon2.kdbx rename to test/src/test/resources/test123-ChaCha20-Argon2.kdbx diff --git a/test/src/main/resources/test123.kdb b/test/src/test/resources/test123.kdb similarity index 100% rename from test/src/main/resources/test123.kdb rename to test/src/test/resources/test123.kdb diff --git a/test/src/main/resources/test123.kdbx b/test/src/test/resources/test123.kdbx similarity index 100% rename from test/src/main/resources/test123.kdbx rename to test/src/test/resources/test123.kdbx diff --git a/test/src/main/resources/testDeleted.kdbx b/test/src/test/resources/testDeleted.kdbx similarity index 100% rename from test/src/main/resources/testDeleted.kdbx rename to test/src/test/resources/testDeleted.kdbx diff --git a/test/src/main/resources/xml/V4-AES-AES.xml b/test/src/test/resources/xml/V4-AES-AES.xml similarity index 100% rename from test/src/main/resources/xml/V4-AES-AES.xml rename to test/src/test/resources/xml/V4-AES-AES.xml diff --git a/test/src/main/resources/xml/V4-AES-Argon2.xml b/test/src/test/resources/xml/V4-AES-Argon2.xml similarity index 100% rename from test/src/main/resources/xml/V4-AES-Argon2.xml rename to test/src/test/resources/xml/V4-AES-Argon2.xml diff --git a/test/src/main/resources/xml/V4-ChaCha20-AES.xml b/test/src/test/resources/xml/V4-ChaCha20-AES.xml similarity index 100% rename from test/src/main/resources/xml/V4-ChaCha20-AES.xml rename to test/src/test/resources/xml/V4-ChaCha20-AES.xml diff --git a/test/src/main/resources/xml/V4-ChaCha20-Argon2-Attachment.xml b/test/src/test/resources/xml/V4-ChaCha20-Argon2-Attachment.xml similarity index 100% rename from test/src/main/resources/xml/V4-ChaCha20-Argon2-Attachment.xml rename to test/src/test/resources/xml/V4-ChaCha20-Argon2-Attachment.xml diff --git a/util/pom.xml b/util/pom.xml new file mode 100644 index 00000000..dbb83f82 --- /dev/null +++ b/util/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + org.linguafranca.pwdb + KeePassJava2-parent + 2.2.3-SNAPSHOT + + + PWDB :: Util + Some underlying utilities which are needed for running tests. + util + + + 11 + 11 + UTF-8 + + + + + junit + junit + + + + \ No newline at end of file diff --git a/database/src/main/java/org/linguafranca/util/MavenRunListener.java b/util/src/main/java/org/linguafranca/test/maven/MavenRunListener.java similarity index 98% rename from database/src/main/java/org/linguafranca/util/MavenRunListener.java rename to util/src/main/java/org/linguafranca/test/maven/MavenRunListener.java index 1839a645..9881b221 100644 --- a/database/src/main/java/org/linguafranca/util/MavenRunListener.java +++ b/util/src/main/java/org/linguafranca/test/maven/MavenRunListener.java @@ -1,4 +1,4 @@ -package org.linguafranca.util; +package org.linguafranca.test.maven; import org.junit.runner.Description; import org.junit.runner.notification.Failure; diff --git a/test/src/main/java/org/linguafranca/util/HexViewer.java b/util/src/main/java/org/linguafranca/test/util/HexViewer.java similarity index 70% rename from test/src/main/java/org/linguafranca/util/HexViewer.java rename to util/src/main/java/org/linguafranca/test/util/HexViewer.java index 2ec49e0c..f23538aa 100644 --- a/test/src/main/java/org/linguafranca/util/HexViewer.java +++ b/util/src/main/java/org/linguafranca/test/util/HexViewer.java @@ -1,10 +1,10 @@ -package org.linguafranca.util; +package org.linguafranca.test.util; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; -import static org.linguafranca.util.TestUtil.getTestPrintStream; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * Useful for e.g. viewing the raw file contents @@ -13,14 +13,6 @@ public class HexViewer { static PrintStream printStream = getTestPrintStream(); - public static void main(String[] args) throws IOException { - - - try (InputStream is = HexViewer.class.getClassLoader().getResourceAsStream("V4-AES-Argon2-CustomIcon.kdbx");) { - list(is); - } - } - public static void list(InputStream is) throws IOException { for (int i = 0; i < 32; i++) { byte[] buf = new byte [16]; diff --git a/util/src/main/java/org/linguafranca/test/util/TestUtil.java b/util/src/main/java/org/linguafranca/test/util/TestUtil.java new file mode 100644 index 00000000..7476ddc4 --- /dev/null +++ b/util/src/main/java/org/linguafranca/test/util/TestUtil.java @@ -0,0 +1,27 @@ +package org.linguafranca.test.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; + +public class TestUtil { + /** + * Do nothing output stream + */ + public static class NullOutputStream extends OutputStream { + + @Override + public void write(int b) throws IOException { + + } + } + /** + * set system property to suppress output from tests + * @return if "inhibitConsoleOutput" has been set, e.g. in a profile + */ + public static PrintStream getTestPrintStream() { + return Boolean.getBoolean("inhibitConsoleOutput") ? + new PrintStream(new NullOutputStream()) : + new PrintStream(System.out); + } +} From 65e9841f8ae1e327c4608fa93852033f25b9340d Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 19 Nov 2024 21:30:49 +0000 Subject: [PATCH 204/228] Fixes for issue #70 --- CHANGELOG.md | 2 + .../pwdb/kdbx/validation/Issue70Test.java | 66 ++++++++++++++++++ .../pwdb/kdbx/jackson/model/KeePassFile.java | 2 + .../pwdb/kdbx/simple/model/KeePassFile.java | 1 + test/src/test/resources/issue-70/test1.kdbx | Bin 0 -> 1749 bytes test/src/test/resources/issue-70/test2.kdbx | Bin 0 -> 2071 bytes 6 files changed, 71 insertions(+) create mode 100644 example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue70Test.java create mode 100644 test/src/test/resources/issue-70/test1.kdbx create mode 100644 test/src/test/resources/issue-70/test2.kdbx diff --git a/CHANGELOG.md b/CHANGELOG.md index c7a0aead..b388ab27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.co - Jackson implementation supports this interface - Other implementations throw exceptions appropriately as unsupported +- [Issue #70] Fix exception if CustomData contains content (in Jackson and Simple) +- [Issue #71] Restructured to avoid pulling in Junit unnecessarily etc. ## [2.2.2] 2024-09-06 diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue70Test.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue70Test.java new file mode 100644 index 00000000..f0c9c28e --- /dev/null +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue70Test.java @@ -0,0 +1,66 @@ +package org.linguafranca.pwdb.kdbx.validation; + + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.StreamFormat; +import org.linguafranca.pwdb.kdbx.Helpers; +import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.Util; +import org.linguafranca.pwdb.kdbx.dom.DomDatabaseWrapper; +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; +import org.linguafranca.pwdb.kdbx.jaxb.JaxbDatabase; +import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; +import org.linguafranca.test.util.TestUtil; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * Review Issue-33 https://github.com/jorabin/KeePassJava2/issues/70 + */ +public class Issue70Test { + + public static final String TEST_RESOURCE1 = "issue-70/test2.kdbx"; + public static final KdbxCreds CREDENTIALS = new KdbxCreds("KeePassJava2".getBytes()); + + + InputStream inputStream; + + @BeforeClass + public static void listXml() throws IOException { + Util.listXml(TEST_RESOURCE1, CREDENTIALS, new PrintWriter(TestUtil.getTestPrintStream())); + Helpers.isV4.set(true); + } + + @Before + public void refreshInputStream() { + inputStream = this.getClass().getClassLoader().getResourceAsStream(TEST_RESOURCE1); + } + + @Test + public void testDomDatabaseWrapper() throws IOException { + DomDatabaseWrapper database = DomDatabaseWrapper.load(CREDENTIALS, inputStream); + } + + @Test + public void testJaxbDatabase() throws IOException { + JaxbDatabase database = JaxbDatabase.load(CREDENTIALS, inputStream); + } + + @Test + public void testSimpleDatabase() throws IOException { + SimpleDatabase database = SimpleDatabase.load(CREDENTIALS, inputStream); + } + + @Test + public void testJacksonDatabase() throws IOException { + JacksonDatabase database = JacksonDatabase.load(CREDENTIALS, inputStream); + } +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java index 6c7691bd..8836d272 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.UUID; +import com.fasterxml.jackson.annotation.JsonIgnoreType; import org.linguafranca.pwdb.Entry; import org.linguafranca.pwdb.kdbx.jackson.JacksonGroup; import org.linguafranca.pwdb.kdbx.jackson.converter.Base64ToByteConverter; @@ -337,6 +338,7 @@ public String getName() { } } + @JsonIgnoreType public static class CustomData { public CustomData() { diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java index 298f512c..3883eca1 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java @@ -249,6 +249,7 @@ public void setContent(byte[] content) { } } + @org.simpleframework.xml.Root(strict=false) public static class CustomData { protected List any; } diff --git a/test/src/test/resources/issue-70/test1.kdbx b/test/src/test/resources/issue-70/test1.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..d27f37d1b1148ddb9245ddaa46b2f1746ae44804 GIT binary patch literal 1749 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 Date: Fri, 22 Nov 2024 13:51:01 +0000 Subject: [PATCH 205/228] Fixes for #70 --- .../pwdb/kdbx/validation/Issue70Test.java | 59 ++++------------- .../pwdb/kdbx/jackson/JacksonEntry.java | 60 ++++++++++-------- .../pwdb/kdbx/jackson/JacksonGroup.java | 27 ++++++-- .../pwdb/kdbx/jackson/model/KeePassFile.java | 27 +++++--- test/src/test/resources/Database-4.1-123.kdbx | Bin 0 -> 2759 bytes 5 files changed, 85 insertions(+), 88 deletions(-) create mode 100644 test/src/test/resources/Database-4.1-123.kdbx diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue70Test.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue70Test.java index f0c9c28e..5bb0f4ad 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue70Test.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue70Test.java @@ -1,66 +1,33 @@ package org.linguafranca.pwdb.kdbx.validation; - -import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; -import org.linguafranca.pwdb.Credentials; -import org.linguafranca.pwdb.StreamFormat; -import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.KdbxCreds; -import org.linguafranca.pwdb.kdbx.Util; -import org.linguafranca.pwdb.kdbx.dom.DomDatabaseWrapper; import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; -import org.linguafranca.pwdb.kdbx.jaxb.JaxbDatabase; -import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; -import org.linguafranca.test.util.TestUtil; -import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.PrintWriter; -import java.nio.file.Files; -import java.nio.file.Paths; /** - * Review Issue-33 https://github.com/jorabin/KeePassJava2/issues/70 + * Review Issue-70 https://github.com/jorabin/KeePassJava2/issues/70 */ public class Issue70Test { public static final String TEST_RESOURCE1 = "issue-70/test2.kdbx"; - public static final KdbxCreds CREDENTIALS = new KdbxCreds("KeePassJava2".getBytes()); - - - InputStream inputStream; - - @BeforeClass - public static void listXml() throws IOException { - Util.listXml(TEST_RESOURCE1, CREDENTIALS, new PrintWriter(TestUtil.getTestPrintStream())); - Helpers.isV4.set(true); - } - - @Before - public void refreshInputStream() { - inputStream = this.getClass().getClassLoader().getResourceAsStream(TEST_RESOURCE1); - } - - @Test - public void testDomDatabaseWrapper() throws IOException { - DomDatabaseWrapper database = DomDatabaseWrapper.load(CREDENTIALS, inputStream); - } - - @Test - public void testJaxbDatabase() throws IOException { - JaxbDatabase database = JaxbDatabase.load(CREDENTIALS, inputStream); - } + public static final KdbxCreds CREDENTIALS1 = new KdbxCreds("KeePassJava2".getBytes()); + public static final String TEST_RESOURCE2 = "Database-4.1-123.kdbx"; + public static final KdbxCreds CREDENTIALS2 = new KdbxCreds("123".getBytes()); @Test - public void testSimpleDatabase() throws IOException { - SimpleDatabase database = SimpleDatabase.load(CREDENTIALS, inputStream); + public void testCustomData() throws IOException { + try (InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(TEST_RESOURCE1)) { + JacksonDatabase database = JacksonDatabase.load(CREDENTIALS1, inputStream); + } } @Test - public void testJacksonDatabase() throws IOException { - JacksonDatabase database = JacksonDatabase.load(CREDENTIALS, inputStream); + public void testFileFormat_4_1() throws IOException { + try (InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(TEST_RESOURCE2)){ + JacksonDatabase database = JacksonDatabase.load(CREDENTIALS2, inputStream); + } } -} +} \ No newline at end of file diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java index f6eb7c57..d585ab28 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java @@ -23,6 +23,7 @@ import org.jetbrains.annotations.NotNull; +import org.linguafranca.pwdb.Icon; import org.linguafranca.pwdb.PropertyValue; import org.linguafranca.pwdb.base.AbstractEntry; import org.linguafranca.pwdb.kdbx.Helpers; @@ -64,7 +65,32 @@ @JsonIgnoreProperties({"path", "username", "title", "notes", "url", "password"}) -public class JacksonEntry extends AbstractEntry { +public class JacksonEntry extends AbstractEntry{ + + @JsonIgnore + JacksonDatabase database; + + @JsonIgnore + JacksonGroup parent; + + protected JacksonEntry() { + string = new ArrayList<>(); + binary = new ArrayList<>(); + times = new Times(); + uuid = UUID.randomUUID(); + iconID = 0; + } + + public static JacksonEntry createEntry(JacksonDatabase database) { + JacksonEntry result = new JacksonEntry(); + result.database = database; + result.parent = null; + // avoiding setProperty as it does a touch(); + for (String p : STANDARD_PROPERTY_NAMES) { + result.string.add(new StringProperty(p, database.getPropertyValueStrategy().newUnprotected().of(""))); + } + return result; + } @JacksonXmlProperty(localName = "UUID") @@ -88,7 +114,12 @@ public class JacksonEntry extends AbstractEntry(); - binary = new ArrayList<>(); - times = new Times(); - uuid = UUID.randomUUID(); - iconID = 0; - } - - public static JacksonEntry createEntry(JacksonDatabase database) { - JacksonEntry result = new JacksonEntry(); - result.database = database; - result.parent = null; - // avoiding setProperty as it does a touch(); - for (String p : STANDARD_PROPERTY_NAMES) { - result.string.add(new StringProperty(p, database.getPropertyValueStrategy().newUnprotected().of(""))); - } - return result; - } - @Override @JsonIgnore public String getProperty(String s) { diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java index f71ffb3d..d0d70f0c 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java @@ -52,7 +52,7 @@ "group", }) @JsonIgnoreProperties(ignoreUnknown=true) -public class JacksonGroup extends AbstractGroup { +public class JacksonGroup extends AbstractGroup { @JacksonXmlProperty(localName = "UUID") @JsonDeserialize(converter = Base64ToUUIDConverter.class) @@ -99,6 +99,14 @@ public class JacksonGroup extends AbstractGroup entries; @@ -114,7 +122,7 @@ public class JacksonGroup extends AbstractGroup(); groups = new ArrayList<>(); times = new Times(); @@ -145,7 +153,8 @@ public JacksonGroup getParent() { } @Override - public void setParent(JacksonGroup group) { + public void setParent(JacksonGroup g) { + JacksonGroup group = (JacksonGroup) g; if (isRootGroup()) { throw new IllegalStateException("Cannot add root group to another group"); } @@ -173,7 +182,8 @@ public int getGroupsCount() { } @Override - public JacksonGroup addGroup(JacksonGroup group) { + public JacksonGroup addGroup(JacksonGroup g) { + JacksonGroup group = (JacksonGroup) g; if (group.isRootGroup()) { throw new IllegalStateException("Cannot add root group to another group"); } @@ -190,7 +200,8 @@ public JacksonGroup addGroup(JacksonGroup group) { } @Override - public JacksonGroup removeGroup(JacksonGroup group) { + public JacksonGroup removeGroup(JacksonGroup g) { + JacksonGroup group = (JacksonGroup) g; if (this.database != group.database) { throw new IllegalStateException("Must be from same database"); } @@ -212,7 +223,8 @@ public int getEntriesCount() { } @Override - public JacksonEntry addEntry(JacksonEntry entry) { + public JacksonEntry addEntry(JacksonEntry e) { + JacksonEntry entry = (JacksonEntry) e; if (this.database != entry.database) { throw new IllegalStateException("Must be from same database"); } @@ -226,7 +238,8 @@ public JacksonEntry addEntry(JacksonEntry entry) { } @Override - public JacksonEntry removeEntry(JacksonEntry entry) { + public JacksonEntry removeEntry(JacksonEntry e) { + JacksonEntry entry = (JacksonEntry) e; if (this.database != entry.database) { throw new IllegalStateException("Must be from same database"); } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java index 8836d272..7cef5274 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java @@ -21,7 +21,6 @@ import java.util.List; import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonIgnoreType; import org.linguafranca.pwdb.Entry; import org.linguafranca.pwdb.kdbx.jackson.JacksonGroup; import org.linguafranca.pwdb.kdbx.jackson.converter.Base64ToByteConverter; @@ -338,20 +337,32 @@ public String getName() { } } - @JsonIgnoreType public static class CustomData { - public CustomData() { + public static class CustomDataItem { + @JacksonXmlProperty(localName = "Key") + public String key; + @JacksonXmlProperty(localName = "Value") + public String value; + @JacksonXmlProperty(localName = "LastModificationTime") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + public Date lastModificationTime; } - public List getAny() { - return any; + @JacksonXmlProperty(localName = "Item") + @JacksonXmlElementWrapper(useWrapping = false) + protected List items; + + public CustomData() { } - public void setAny(List any) { - this.any = any; + public List getItems() { + return items; } - protected List any; + public void setItems(List items) { + this.items = items; + } } } diff --git a/test/src/test/resources/Database-4.1-123.kdbx b/test/src/test/resources/Database-4.1-123.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..024017e1559c798666c0119016ba62f879599483 GIT binary patch literal 2759 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 N(V?M!1wjA+004bUJc$4R literal 0 HcmV?d00001 From c6a588f0a6aff26ef764f1e8725faece940af372 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 22 Nov 2024 14:01:45 +0000 Subject: [PATCH 206/228] Adding deploy skip to test and util ref #71 --- test/pom.xml | 4 ++++ util/pom.xml | 4 +--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/pom.xml b/test/pom.xml index e9cc213a..6dfabd14 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -11,6 +11,10 @@ PWDB :: Test Contains the shared test classes for exercising the functionality of a PWDB database implementation. + + true + + org.linguafranca.pwdb diff --git a/util/pom.xml b/util/pom.xml index dbb83f82..3893e1a4 100644 --- a/util/pom.xml +++ b/util/pom.xml @@ -14,9 +14,7 @@ util - 11 - 11 - UTF-8 + true From ae3ed7b422c8a9510341a7df29f81333c55778c2 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 22 Nov 2024 15:25:57 +0000 Subject: [PATCH 207/228] update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b388ab27..0ef4ab5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.co - Jackson implementation supports this interface - Other implementations throw exceptions appropriately as unsupported -- [Issue #70] Fix exception if CustomData contains content (in Jackson and Simple) +- [Issue #70] Support for KDBX 4.1 format (Jackson) - [Issue #71] Restructured to avoid pulling in Junit unnecessarily etc. ## [2.2.2] 2024-09-06 From 92749dd73193fe9a65f94eac98ab19f1900176fb Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 3 Jan 2025 14:36:30 +0000 Subject: [PATCH 208/228] tweaks to viewing XML --- .../org/linguafranca/pwdb/kdbx/ChooseFile.java | 14 ++++++++------ .../java/org/linguafranca/pwdb/kdbx/SaxParse.java | 1 + 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java index d8645fda..6b376aac 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java @@ -8,18 +8,17 @@ import javax.swing.filechooser.FileFilter; import java.io.*; import java.net.URL; +import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Paths; -import static org.linguafranca.test.util.TestUtil.getTestPrintStream; +import static org.linguafranca.pwdb.kdbx.Util.streamToString; /** * Utility to allow browsing of database files and listing content to console */ public class ChooseFile { - OutputStream outputStream = getTestPrintStream(); - public static void main(String[] args) throws IOException { ChooseFile cf = new ChooseFile(); cf.choose(); @@ -66,9 +65,12 @@ public String getDescription() { return; } logger.info("Opening {}", fc.getSelectedFile().getPath()); - Util.listXml(fc.getSelectedFile().getName(), - new KdbxCreds(s.getBytes()), - new PrintWriter(outputStream)); + try (InputStream is = Files.newInputStream(Paths.get(fc.getSelectedFile().getPath()))) { + InputStream ss = KdbxSerializer.createUnencryptedInputStream(new KdbxCreds(s.getBytes()), new KdbxHeader(), is); + System.out.println(streamToString(ss)); + System.out.println(); + System.out.flush(); + } /* try (InputStream is = Files.newInputStream(Paths.get(fc.getSelectedFile().getPath()))) { HexViewer.list(is); }*/ diff --git a/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java index 2128b9d2..caa7ac8b 100644 --- a/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java +++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java @@ -89,5 +89,6 @@ public void characters(char[] ch, int start, int length) { InputSource xmlInputSource = new InputSource(decryptedInputStream); xmlReader.parse(xmlInputSource); } + writer.flush(); } } From 30561dd74bdcc7c8d8e71ef0b4968457b7af7c17 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 3 Jan 2025 14:38:07 +0000 Subject: [PATCH 209/228] documentation --- PropertyValueProtection.md | 4 ++-- pom.xml | 1 - readme.md | 25 +++++++++++++++++-------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/PropertyValueProtection.md b/PropertyValueProtection.md index ccb6f87d..39e130ef 100644 --- a/PropertyValueProtection.md +++ b/PropertyValueProtection.md @@ -8,7 +8,7 @@ that have sensitive values are called "Protected Properties". KDBX files contain a list of the standard properties and whether they are to be treated as protected by default. The standard properties include Password, which is so treated. There doesn't seem to be a way of updating this list in the Windows KeePass implementation, and in any case documentation says that the -list is updated to default after load of a database, so it seems that it ignored. +list is updated to default after load of a database, so it seems that it is ignored. Individual properties can be marked as "protect in memory", but there is no way to indicate that all properties of that name should be protected. @@ -63,7 +63,7 @@ luck trying to avoid strings at all. Likewise, if you collect passwords from a d After the deserialization process, the passwords are stored in RAM. Therefore: **Hashing the password is not feasible**, as the hash function is a one-way process. -This means that once a password is hashed, it cannot be reverted back to its original form, +This means that once a password is hashed, it cannot be reverted to its original form, which makes hashing unsuitable in this context. Since we need to retrieve and view the saved passwords (as per the requirements), hashing does not meet the objective. diff --git a/pom.xml b/pom.xml index f064f123..3f5c468f 100644 --- a/pom.xml +++ b/pom.xml @@ -212,7 +212,6 @@ junit junit 4.13.2 - diff --git a/readme.md b/readme.md index fa4bcaca..cc9516a5 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent) [![javadoc](https://javadoc.io/badge2/org.linguafranca.pwdb/KeePassJava2/javadoc.svg)](https://javadoc.io/doc/org.linguafranca.pwdb/KeePassJava2) -![alt text](https://badgen.net/badge/Build/2.2.2/blue?icon=github) +![alt text](https://badgen.net/badge/Build/2.2.3/blue?icon=github) master [![CircleCI](https://dl.circleci.com/status-badge/img/gh/jorabin/KeePassJava2/tree/master.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/jorabin/KeePassJava2/tree/master) develop [![CircleCI](https://dl.circleci.com/status-badge/img/gh/jorabin/KeePassJava2/tree/develop.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/jorabin/KeePassJava2/tree/develop) @@ -39,7 +39,7 @@ It is licensed under the Apache 2 License and is currently usable. ## Current Status -The current code is version 2.2.2 - released to Maven September 2024. This is on the main branch. See [Build from Source](#build-from-source) +The current code is version 2.2.3 - released to Maven December 2024. This is on the main branch. See [Build from Source](#build-from-source) Key updates relative to 2.1: - Java 8 (dependencies no longer support Java 7) @@ -54,21 +54,30 @@ See the [changelog](CHANGELOG.md) for more details. ### Release -The composite POM for the last release (2.2.2), Java 8 compatible, is +The POM for the last release (2.2.3), Java 8 compatible, is + + org.linguafranca.pwdb + KeePassJava2-jackson + 2.2.3 + +at Maven Central. This provides access to the Jackson based implementation, +which is now the recommended implementation. There is also a composite POM that +provides access to all implementations (see [below](#database-implementations) for discussion). org.linguafranca.pwdb KeePassJava2 - 2.2.2 + 2.2.3 -at Maven Central. Note that the artifactId has become Camel Case from release 2.1.x onwards. +Note that the artifactId has become Camel Case from release 2.1.x onwards. ### Snapshot -Snapshot builds are erratically available at [Sonatype](https://oss.sonatype.org/content/repositories/snapshots/org/linguafranca/pwdb/): +Snapshot builds are erratically available at [Sonatype](https://oss.sonatype.org/content/repositories/snapshots/org/linguafranca/pwdb/), next bug-fix release will be +2.2.4-SNAPSHOT (on branch `develop`) and work-in-progress 3.0.0-SNAPSHOT (on branch `v3`): org.linguafranca.pwdb KeePassJava2 - 2.2.3-SNAPSHOT + 2.2.4-SNAPSHOT with appropriate `` entry, like: @@ -103,7 +112,7 @@ then choose the Jackson based database implementation, and load the database Database database = JacksonDatabase.load(credentials, inputStream) -See below for discussion of other database implementations. Note that they will not be +See [below](#database-implementations) for discussion of other database implementations. Note that they will not be maintained in the future - and see the following regarding making the storage of passwords more secure. From 60ddcd8173f4654537f8bcf387632aec62c112e6 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Fri, 3 Jan 2025 18:03:51 +0000 Subject: [PATCH 210/228] Further fixes for issue #70 - adding CustomData to Entry and Group --- .../pwdb/kdbx/validation/Issue70Test.java | 12 ++++++++++++ .../pwdb/kdbx/jackson/JacksonEntry.java | 5 +++++ .../pwdb/kdbx/jackson/JacksonGroup.java | 7 +++++++ .../test/resources/issue-70/test-hugoo10.kdbx | Bin 0 -> 2215 bytes 4 files changed, 24 insertions(+) create mode 100644 test/src/test/resources/issue-70/test-hugoo10.kdbx diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue70Test.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue70Test.java index 5bb0f4ad..65bfd42e 100644 --- a/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue70Test.java +++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue70Test.java @@ -16,7 +16,11 @@ public class Issue70Test { public static final KdbxCreds CREDENTIALS1 = new KdbxCreds("KeePassJava2".getBytes()); public static final String TEST_RESOURCE2 = "Database-4.1-123.kdbx"; public static final KdbxCreds CREDENTIALS2 = new KdbxCreds("123".getBytes()); + public static final String TEST_RESOURCE3 = "issue-70/test-hugoo10.kdbx"; + public static final KdbxCreds CREDENTIALS3 = new KdbxCreds("test".getBytes()); + + /* custom data missing from meta */ @Test public void testCustomData() throws IOException { try (InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(TEST_RESOURCE1)) { @@ -30,4 +34,12 @@ public void testFileFormat_4_1() throws IOException { JacksonDatabase database = JacksonDatabase.load(CREDENTIALS2, inputStream); } } + + /* custom data missing from entry definition */ + @Test + public void testCustomData2() throws IOException { + try (InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(TEST_RESOURCE3)){ + JacksonDatabase database = JacksonDatabase.load(CREDENTIALS3, inputStream); + } + } } \ No newline at end of file diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java index d585ab28..3913fcab 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java @@ -55,11 +55,13 @@ "foregroundColor", "backgroundColor", "overrideURL", + "previousParentGroup", "tags", "times", "string", "binary", "autoType", + "customData", "history", }) @@ -137,6 +139,9 @@ public static JacksonEntry createEntry(JacksonDatabase database) { @JacksonXmlProperty(localName = "AutoType") protected AutoType autoType; + @JacksonXmlProperty(localName = "CustomData") + protected KeePassFile.CustomData customData; + @JacksonXmlProperty(localName = "History") /* Workaround jackson */ protected JacksonHistory history; diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java index d0d70f0c..b98278fb 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java @@ -29,6 +29,7 @@ import org.linguafranca.pwdb.kdbx.jackson.converter.BooleanToStringConverter; import org.linguafranca.pwdb.kdbx.jackson.converter.StringToBooleanConverter; import org.linguafranca.pwdb.kdbx.jackson.converter.UUIDToBase64Converter; +import org.linguafranca.pwdb.kdbx.jackson.model.KeePassFile; import org.linguafranca.pwdb.kdbx.jackson.model.Times; import java.util.ArrayList; @@ -48,6 +49,9 @@ "enableAutoType", "enableSearching", "lastTopVisibleEntry", +"previousParentGroup", +"tags", +"customData", "entry", "group", }) @@ -107,6 +111,9 @@ public class JacksonGroup extends AbstractGroup entries; diff --git a/test/src/test/resources/issue-70/test-hugoo10.kdbx b/test/src/test/resources/issue-70/test-hugoo10.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..1eb0185953d24c38828a821c93b307d22ee49a08 GIT binary patch literal 2215 zcmV;Y2w3+6*`k_f`%AR}00aO65C8xGF~RcYzi~rQzE}kzYW!ON0|Wp70096100bZa z0020y2U)2QR-B5>7Y8Ih0KqgrLe80?s7)jO`j*Fv{+J70000000YU`;001OaRY^n; z0002V+4GuWibP<)bPWi>icjhV0RR91QV0M50Kg{+00000073x(002`U0000Pgs;aA{>0T!fbv953 z002v{0y2RNl2MAm(klLFs}CT$c6TZ2ULeETfSCLC7h@#GLkc zKNgA9mU}H1RI;p*=bdg+d82sCGB2tAaFKPSuTs<3k5F*;zRDgexi`-^Oac7`MtmRM zbXSfZH}zGc#t-_4)?qyL?w`&z`_NzopETWqdJZ%AJ)Bi9 zqhfSHB~p1u0QHBqfCydJ(?%ptF;?$hI_9U9UF+M2^_TC5zj$T4ZzTmdr$+r0`vW>a zLEZfTv%)PWbMCv7X=xlxL%$^ys3s{|N&HyIps;YI;A}#Q{|Kr^VYk_s(hDrHrk`^) zB2%{p-*^ov1FaibM#0H1?O}DzH)B*R;E+enYZ1>s4cWyFxEZP)IUkb@Q7u*uowJ5E->>N zcKY=_r#LQVE`cdTBkyRA|7lX0aiYRODNocZ$Iq3Yk9V}}i9hnbYNwA_(BNDJ9~4`G zSWrIkfKUpfsJri*+EltxK9t(>GS)4xsFV3THs84a!j67g_pM}XEYe3f>icRH3XphN zZFqs+>t?yc1v(=UZnRrwoTkECwa_1!68PL@^Tx{iChf87a-r)&Yw(Pzel>s>Ko&=9 z?ugnJfEiI?N6lNJe13;iL&?2)bmX#(*tsZA#`GN{(W0W_Ogt9+{dwD8*8nQrdXV)M z|C%H8mv>wdxg-cd6x-p$8uC;(hL!iQs-^T{-6x1?;v0~`y$K?XHfp}huL4Z#)~FsM zTDcZGWp*hyc>gvWDaB8OP}#@DjejuUB#>#L0i@d12NM_}zl3bj?E&ReiJlPW79fPh zN;yOv+%aoZZD}vm$zbrp-1}!rL22or1z~6ra{J+@#VqI2F%z%As)xesKd3|cyOKB_ zZAOPEy%9@Y$yKT0b(A7S-S?CpvS(>$+qw;`9D+wm~kB&ZCXO$tHLBhbi1gRCr0$94*~T1#Zh#|v7{jsCraU>-j?6J*LB_RL;vFdBnM2P!%_iE<(s0!KDqK}__)*r~GyzU}$xA(|4xk56DKF9=j)a4~Q zKy(XHQzWnZ%VT+cV-StCD{nGiGBX<%2^3wm*pT}K5R1SX&rjU|05dp9{V%km0|4~V zh%v+`6=+3?j;$pCdhEj)16FDOKgQZfrt`XuHOKTnEXsel(EnV5S{QtK7`hM_)7k=5 z@dUv5c&VIC{Brj6F3TD~mm=F>-J{O6){^-pBaTdKlMoT3TK_t^1Mrtk;*t|&7TE8* zRxJyDMP8x@->SrIK}W`e^a!7YRMk$J9|hF)n(IYG{#iyHbZh># z$%bAGcDGvERhr^EI&8tbqkXRsaAr8yyzrt4Ep$WN(!Hcg6x&v2BrW!pek1=y3IIu{ zpvlt2Z^V+%I`F@Am=}-dq9v~i&`YQ^D^~{FG1-nk?f*y(argRIM<`)N6TcDh2-^oa pcCE?j6MmjJtqi1pBu)r%=a3>o_F~2Y#iUQa+xl>IW7z-z006>0F4F)2 literal 0 HcmV?d00001 From 5daf26ee872ce0722a441224578742890e0fe2d8 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sun, 5 Jan 2025 14:55:07 +0000 Subject: [PATCH 211/228] Updating XSD to 4.1 --- KDBX.4.1.reichl.xsd | 492 +++++++++++++++++++++++++++++++++++++ KDBX.4.1.xsd | 583 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1075 insertions(+) create mode 100644 KDBX.4.1.reichl.xsd create mode 100644 KDBX.4.1.xsd diff --git a/KDBX.4.1.reichl.xsd b/KDBX.4.1.reichl.xsd new file mode 100644 index 00000000..a2f4780d --- /dev/null +++ b/KDBX.4.1.reichl.xsd @@ -0,0 +1,492 @@ + + + + KDBX 4.1 XML Schema. Copyright (C) 2007-2025 Dominik Reichl. https://keepass.info/help/kb/kdbx.html + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the application that has generated the XML document. + + + + + Hash of the (unencrypted) header of a KDBX file. Used only in KDBX files prior to version 4. In KDBX ≥ 4, the integrity and the authenticity are ensured using a HMAC instead (see KDBX spec.). + + + + + Last date/time when a database setting (stored in the Meta element) has been changed. + + + + + + + + + User name that is used by default for new entries. + + + + + + Number of days until history entries are deleted in a database maintenance operation. + + + + + Database color. The user interface can colorize elements with this color in order to allow the user to quickly identify the database. + + + + + Last date/time when the master key has been changed. + + + + + Number of days until a change of the master key is recommended. -1 means never. + + + + + + + + + + Number of days until a change of the master key is enforced. -1 means never. + + + + + + + + + + If true, a change of the master key should be enforced once directly after the user opens the database. + + + + + + + + + + + + + + UUID of the group that is used as recycle bin. Zero UUID = create new group when necessary. + + + + + + + + Maximum number of history entries that each entry may have. -1 means unlimited. + + + + + + + + + + Maximum estimated size in bytes (in the process memory) of the history of each entry. -1 means unlimited. + + + + + + + + + + + + In this element, the content of each binary is stored. Used only in unencrypted XML files and in KDBX files prior to version 4. In KDBX ≥ 4, binaries are stored in the inner header (encrypted) instead. + + + + + + + + + + + + + + + + + Process memory protection settings, describing which standard fields should be protected. KeePass resets these settings to their default values after opening a database. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Reference to a binary content stored in the inner header (KDBX file) or in the Meta/Binaries element (unencrypted XML file). + + + + + + + + + + + + + + + + + + Used in a KDBX file. + + + + + Used in an unencrypted XML file. + + + + + + + + + + + If the attribute is true, the content of the element has been encrypted (and Base64-encoded). See "inner encryption" on https://keepass.info/help/kb/kdbx.html + + + + + + Custom data item (key/value pair) for plugins/ports. The key should be unique, e.g. "PluginName_ItemName". + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + When the user deletes an object (group, entry, ...), an item is created in this list. When synchronizing/merging database files, this information can be used to decide whether an object has been deleted. + + + + + + + + + + + + + + + + + + + In general, last access times are not reliable, because an access is not considered to be a database change. See the UIFlags value 0x20000: https://keepass.info/help/v2_dev/customize.html#uiflags + + + + + + + Cf. LastAccessTime. + + + + + Last date/time when the object has been moved (within its parent group or to a different group). This is used by the synchronization algorithm to determine the latest location of the object. + + + + + + + + + + + + + See the folder "Ext/Images_Client_HighRes" in the KeePass source code package. + + + + + Reference to a custom icon stored in the KeePassFile/Meta/CustomIcons element. If non-zero, it overrides IconID. + + + + + + Specifies whether the group is displayed as expanded in the user interface. + + + + + + + + + UUID of the group in which the current group was stored previously. This information can for instance be used by a recycle bin restoration command. + + + + + Tags associated with the group, separated using ';'. https://keepass.info/help/v2/entry.html#tags + + + + + + + + + + + + + + See TGroup/IconID. + + + + + See TGroup/CustomIconUUID. + + + + + + + https://keepass.info/help/base/autourl.html#override + + + + + https://keepass.info/help/v2/entry.html#gen https://keepass.info/help/kb/pw_quality_est.html + + + + + See TGroup/Tags. + + + + + See TGroup/PreviousParentGroup. + + + + + + + + + + https://keepass.info/help/v2/entry.html#hst + + + + + + + + + + + + https://keepass.info/help/base/autotype.html + + + + + + + + + + No obfuscation. + + + + + Two-channel auto-type obfuscation. https://keepass.info/help/v2/autotype_obfuscation.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A hexadecimal CSS color of the form "#RRGGBB". For example, "#FFFF00" is yellow. An empty string means to use the default value (chosen by the application, suitable for the current UI). + + + + + + + + + + + + + + + + + In a KDBX ≥ 4 file, dates are stored as the number of seconds (Int64) elapsed since 0001-01-01 00:00:00 UTC encoded using Base64. In an unencrypted XML file, dates are stored as xs:dateTime. + + + + + + + + + + + + + + + + + A 128-bit UUID encoded using Base64. + + + + + + \ No newline at end of file diff --git a/KDBX.4.1.xsd b/KDBX.4.1.xsd new file mode 100644 index 00000000..419eddd8 --- /dev/null +++ b/KDBX.4.1.xsd @@ -0,0 +1,583 @@ + + + + + + The KDBX 4.1 XML format as used by KeePass 2.x compatible programs. + + This is an update of earlier versions of the schema for versions 3.1 and 4 to be found at ../KDBX.{v}.xsd + + The KDBX schema is documented (with somewhat different conventions) at + https://keepass.info/help/download/KDBX_XML.xsd. This schema was originally created as a) that XSD did + not exist at the time b) this schema was originally created to provide for JAXB bindings and was + tailored for that purpose. + + In this version, artefacts that existed only to allow for JAXB purposes, for example abstract "parented" + elements to make it easier to add transient data in Java, have been removed. Also some correlation was + carried out with the XSD at keepass.info, which as mentioned did not originally exist at the time this + file was originally created. xs:sequence has been changed to xs:all where appropriate. + + Conventions in this schema: + + KeePassFile, Group, Entry, Times and History are top level elements as they may be useful for + creating fragments or something. + + There are types for almost everything that contains documentation as to its purpose + and how it's used and if there are restrictions on the values. + + Everything else is simply included inline for the sake of brevity. Undoubtedly + more documentation could most usefully be added. + + Some elements are probably optional but are not marked as such. In real life they may be serialised + as empty elements and some are serialised with content "null". Actual serialisations will depend on + the view that various implementations have taken, so flexibility is required when loading. Historically + this was particularly true of boolean values. + + + + + + + + The root element. + + + + + + + This contains database settings and also contains information + about the last state of the program that used the database. Presumably assuming + that the same program will be used to open it again, or that a different program + would want/need the same settings. + + + + + + The name of an application. This might be an application + that created the original document or it could be an application that + altered it at some point. + + + + + + Header hash removed from version 4. Hashes now contained + in the kdbx header itself. + + + + + + The last time something in the enclosing meta element changed. + + + + + + + + + + + + + + + + + + When the XML database is loaded into memory, sensitive + fields will be encrypted in that stream. This element specifies which of the + standardFields are encrypted in that stream, which is decrypted using Salsa20 + for KDBX3 (or ChaCha20 by default for V4) and the InnerRandomStream encryption header. + + ./Entry/String/Value/@Protected signifies that the value of a stringField is + so encrypted and applies also to customFields. + + Attachments are optionally memory protected in V4. + + + + + + + + + + + + + + + + + + + + + + + + + Used to store binary values in version 3.1 - in version 4 + such values are now transferred as part of the KDBX inner headers. + + + + + + + + + + + + + + This is the root group which contains everything. + + + + + + When the user permanently deletes an wrappedEntry or a group, + an item is created in this node. This allows the synchronization function + to decide whether an object has been deleted or is just missing in one of + the databases that are being synchronized. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + When it was last updated, resulting in an update to History if that is + active. + + + + + + When it was originally created. + + + + + When was last used (read). + + + + + When it expires if it expires. + + + + + True if it expires. + + + + + How often the object has been used (e.g. by copying data of it to the + clipboard, performing auto-type with it, etc.). However, a change to the usage count is + not considered to be a database change (i.e. the database is not marked as changed by the + UI), thus this value is typically incorrect (and the same applies to the last access + time). + + + + + + The date/time when the object was last moved. This allows the + synchronization function to correctly determine the most recent location of the + object. + + + + + + + + + + + + + + + + + + + + + I think the preferred values are "True" and "False", but for the sake of accommodating + various non-conformant applications ... accept a wider variety. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This is ISO Date time stored as UTC (Z time zone). + + In KDBX Version 4 this becomes a base64 encoded offset in seconds + from 0001-01-01T00:00:00Z - if stored in a KDBX encoded file, but + is a xs:dateTime otherwise. There is no attribute to say what the format is + so be prepared to do either. + + + + + + + + + + + + + + XSD doesn't do comma (or semicolon) separated lists, so we can't model this handily. + + Assume that the format is something like [{token}{[,;] token}*]+ where white space is ignored except + as part of the name of the token. + + See https://keepass.info/help/v2/entry.html#tags + + + + + + + + + This is where the values of the database are actually stored. You can have String valued + fields, and you can have Binary valued fields. There are "Default" String fields (username and so on) and + there are custom string fields (custom only in that their names are not the names of default string + fields). Not really clear whether the keys are case-sensitive. + + Note that the "Protected" attribute is used in encrypted KDBX files and "ProtectInMemory" is used + in unencrypted XML files. + + + + + + + + + + + + + + + + + + + + If derived from a file then the Key element conventionally contains the file name. + I think it would be useful to add a mime type on this ... + + The Value element is the value of /./meta/binaries/binary/@ID that is referenced from this + binary (V3 and XML formatted documents). Or in the inner header of a KDBX file. + + + + + + + + + + + + + + + + + An index to one of the 69 built in icons. + + + + + + + + + + + + Simply put: "#rrggbb". + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Specifies the override URL, as documented at + http://keepass.info/help/base/autourl.html#override + + + + + + + + + + Stores everything related to auto-type for this wrappedEntry. + http://keepass.info/help/base/autotype.html + + + + + + + 1 if Two-Channel Auto-Type Obfuscation is enabled, otherwise 0. + http://keepass.info/help/v2/autotype_obfuscation.html + + + + + + + + + + + + + Custom window/sequence association + + + + + + + + + + + + + + + + Number of days before triggering an event e.g. suggesting a password change. + -1 means never. 0 means every time. + + + + + + + + + + + + A base64 encoded UUID. Each Group and Entry has a UUID. When added to History the same + UUID is maintained. + + + + + + + + A reference to the UUID e.g. a Group like the Recycle bin. If, for + example, the recycle bin is enabled but has not been used, then it won't have been + created and the value stored here is a 0 UUID. + + + + + + + + A reference to the custom icon UUID found in the meta/customIcons list. + + + + + + + + + Binary field for elements are centrally stored in this element. + The same Binary element may be referenced from many Entries. + + + + + + The ID attribute is referenced in the Ref attribute of + a corresponding Binary element of an Entry. The Compressed attribute specifies + whether the base64 encoded value is to be decompressed to recover the binary + data. + + + + + + + + + + + + + + + + + + + + Third party programs and plugins can put custom data here. + Unique element names should be used, e.g. "PluginName_ItemName". + + + + + + + Note that "LastModificationTime" is actually only allowed when this appears in "Meta" + and not when it appears in "Entry" or "Group" + + + + + + + + + + + + + + \ No newline at end of file From 54f9ce536cd4398c879521152fe26fdaf189a899 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sun, 5 Jan 2025 14:58:24 +0000 Subject: [PATCH 212/228] Adding various missing properties to Jackson implementation only, see #70 --- CHANGELOG.md | 2 +- .../linguafranca/pwdb/kdbx/jackson/JacksonEntry.java | 7 +++++++ .../pwdb/kdbx/jackson/JacksonSerializableDatabase.java | 10 ++++++++++ .../pwdb/kdbx/jackson/model/KeePassFile.java | 6 ++++++ .../org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java | 2 +- 5 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ef4ab5c..e1387a3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.co - Jackson implementation supports this interface - Other implementations throw exceptions appropriately as unsupported -- [Issue #70] Support for KDBX 4.1 format (Jackson) +- [Issue #70] Improved support for KDBX 4.1 format (Jackson only) - [Issue #71] Restructured to avoid pulling in Junit unnecessarily etc. ## [2.2.2] 2024-09-06 diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java index 3913fcab..2f65db59 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java @@ -28,6 +28,8 @@ import org.linguafranca.pwdb.base.AbstractEntry; import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.jackson.converter.Base64ToUUIDConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.BooleanToStringConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.StringToBooleanConverter; import org.linguafranca.pwdb.kdbx.jackson.converter.UUIDToBase64Converter; import org.linguafranca.pwdb.kdbx.jackson.model.KeePassFile; @@ -124,6 +126,11 @@ public static JacksonEntry createEntry(JacksonDatabase database) { @JacksonXmlProperty(localName = "Tags") protected String tags; + + @JacksonXmlProperty(localName = "QualityCheck") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) + protected Boolean qualityCheck; @JacksonXmlProperty(localName = "Times") protected Times times; diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java index a3f9d10a..356de849 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java @@ -17,6 +17,7 @@ import com.ctc.wstx.api.WstxInputProperties; import com.ctc.wstx.api.WstxOutputProperties; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.module.SimpleModule; @@ -40,6 +41,12 @@ public class JacksonSerializableDatabase implements SerializableDatabase { + /** + * By default, deserialization will fail if an unknown property is found. Historically, + * this has been because the file mapping is incomplete, rather than the incoming file being wrong. + * So use this feature with caution, if at all. + */ + public static boolean FAIL_ON_UNKNOWN_PROPERTIES = true; public KeePassFile keePassFile; private StreamEncryptor encryptor; @@ -64,6 +71,8 @@ public JacksonSerializableDatabase(KeePassFile keePassFile) { @Override public JacksonSerializableDatabase load(InputStream inputStream) throws IOException { XmlMapper mapper = new XmlMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, + JacksonSerializableDatabase.FAIL_ON_UNKNOWN_PROPERTIES); SimpleModule module = new SimpleModule(); module.addDeserializer(PropertyValue.class, new ValueDeserializer(encryptor, propertyValueStrategy)); mapper.registerModule(module); @@ -137,6 +146,7 @@ public static void addBinary(KeePassFile keePassFile, int index, byte[] payload) keePassFile.meta.binaries.add(newBin); } + // TODO this gets binary at index but does not get binary with ID @Override public byte[] getBinary(int index) { KeePassFile.Binary binary = keePassFile.meta.binaries.get(index); diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java index 7cef5274..fa591f66 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java @@ -83,6 +83,7 @@ public Root() { "masterKeyChanged", "masterKeyChangeRec", "masterKeyChangeForce", + "masterKeyChangeForceOnce", "memoryProtection", "customIcons", "recycleBinEnabled", @@ -150,6 +151,11 @@ public Meta() { @JacksonXmlProperty(localName = "MasterKeyChangeForce") protected int masterKeyChangeForce; + @JacksonXmlProperty(localName = "MasterKeyChangeForceOnce") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) + protected Boolean masterKeyChangeForceOnce; + @JacksonXmlProperty(localName = "MemoryProtection") public KeePassFile.MemoryProtection memoryProtection; diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java index c6d3c730..50f95041 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java @@ -80,7 +80,6 @@ public void load(SerializableDatabase serializableDatabase, Credentials credenti public void save(SerializableDatabase serializableDatabase, Credentials credentials, OutputStream outputStream) throws IOException { Helpers.isV4.set(kdbxHeader.getVersion() == 4); if (kdbxHeader.getVersion() == 4) { - // TODO this assumes that the indexes start from 0 and are in sequence ... for (int a = 0; a < serializableDatabase.getBinaryCount(); a++) { int attachmentLength = serializableDatabase.getBinary(a).length; byte[] binary = new byte[attachmentLength + 1]; @@ -88,6 +87,7 @@ public void save(SerializableDatabase serializableDatabase, Credentials credenti System.arraycopy(serializableDatabase.getBinary(a),0, binary, 1, attachmentLength); kdbxHeader.addBinary(binary); } + // TODO the binaries should now be removed so they don't get serialized in XML } try (OutputStream encryptedOutputStream = KdbxSerializer.createEncryptedOutputStream(credentials, kdbxHeader, outputStream)) { From fa891eb7d5afa85397f12cc0047fb0411fbb9815 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sun, 5 Jan 2025 15:47:45 +0000 Subject: [PATCH 213/228] Update logback dependency --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 3f5c468f..7bb830f6 100644 --- a/pom.xml +++ b/pom.xml @@ -194,14 +194,14 @@ org.slf4j slf4j-api - 2.0.12 + 2.0.16 ch.qos.logback logback-classic - 1.3.14 + 1.3.15 commons-codec From 808f382306656cc767fdbb371c37f9f80818c757 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sun, 5 Jan 2025 17:23:59 +0000 Subject: [PATCH 214/228] Update to 2.2.3 and release --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jackson/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 4 ++-- readme.md | 2 +- simple/pom.xml | 2 +- test/pom.xml | 4 ++-- util/pom.xml | 2 +- 13 files changed, 15 insertions(+), 15 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index a0748435..f4c09081 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3-SNAPSHOT + 2.2.3 ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index 55c6a466..512f183a 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3-SNAPSHOT + 2.2.3 ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index dbc81766..e0e91c4f 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3-SNAPSHOT + 2.2.3 ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index 27f5e5e8..72394570 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3-SNAPSHOT + 2.2.3 ../pom.xml 4.0.0 diff --git a/jackson/pom.xml b/jackson/pom.xml index b2a96012..d098b8a7 100644 --- a/jackson/pom.xml +++ b/jackson/pom.xml @@ -18,7 +18,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3-SNAPSHOT + 2.2.3 ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index a8dc4d3d..cca527d5 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3-SNAPSHOT + 2.2.3 ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index 07672354..03e85556 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3-SNAPSHOT + 2.2.3 ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index d66e028c..b548d6ea 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3-SNAPSHOT + 2.2.3 ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index 7bb830f6..1e7e7d39 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.2.3-SNAPSHOT + 2.2.3 database test @@ -52,7 +52,7 @@ scm:git:git://github.com/jorabin/KeePassJava2.git scm:git:git@github.com:jorabin/KeePassJava2.git https://github.com/jorabin/KeePassJava2 - KeePassJava2-2.2.2 + KeePassJava2-2.2.3 diff --git a/readme.md b/readme.md index cc9516a5..4c3dca47 100644 --- a/readme.md +++ b/readme.md @@ -39,7 +39,7 @@ It is licensed under the Apache 2 License and is currently usable. ## Current Status -The current code is version 2.2.3 - released to Maven December 2024. This is on the main branch. See [Build from Source](#build-from-source) +The current code is version 2.2.3 - released to Maven January 2025. This is on the main branch. See [Build from Source](#build-from-source) Key updates relative to 2.1: - Java 8 (dependencies no longer support Java 7) diff --git a/simple/pom.xml b/simple/pom.xml index 535242f4..f23fb75a 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3-SNAPSHOT + 2.2.3 ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index 6dfabd14..d55c0835 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3-SNAPSHOT + 2.2.3 4.0.0 @@ -24,7 +24,7 @@ org.linguafranca.pwdb util - 2.2.3-SNAPSHOT + 2.2.3 test diff --git a/util/pom.xml b/util/pom.xml index 3893e1a4..9ef532aa 100644 --- a/util/pom.xml +++ b/util/pom.xml @@ -6,7 +6,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.2.3-SNAPSHOT + 2.2.3 PWDB :: Util From 42cc6b00f558e8e0df27bf18778d17121444612a Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sun, 5 Jan 2025 17:33:45 +0000 Subject: [PATCH 215/228] Javadoc Problem on release --- kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java index c3f7004e..973fe4f5 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java @@ -62,7 +62,6 @@ public class KdbxKeyFile { *

    * The following comes from KeePass Help Files: *

    - * * Formats. KeePass supports the following key file formats: *
      *
    • XML (recommended, default). There is an XML format for key files. KeePass 2.x uses this format by @@ -82,7 +81,6 @@ public class KdbxKeyFile { * cryptographic hash function in order to build a key (typically a 256-bit key with SHA-256). * This allows to use arbitrary files as key files.
    • *
    - *
    * @param inputStream the input stream holding the key, caller should close * @return the key */ From f451356d10c47a46cbf063e5f1cc24a73c82c201 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sun, 5 Jan 2025 17:38:58 +0000 Subject: [PATCH 216/228] Update to 2.2.4-SNAPSHOT --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jackson/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 2 +- simple/pom.xml | 2 +- test/pom.xml | 4 ++-- util/pom.xml | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index f4c09081..cda4bc26 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3 + 2.2.4-SNAPSHOT ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index 512f183a..457f32aa 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3 + 2.2.4-SNAPSHOT ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index e0e91c4f..00b6beaa 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3 + 2.2.4-SNAPSHOT ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index 72394570..e2334d12 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3 + 2.2.4-SNAPSHOT ../pom.xml 4.0.0 diff --git a/jackson/pom.xml b/jackson/pom.xml index d098b8a7..70f3a02b 100644 --- a/jackson/pom.xml +++ b/jackson/pom.xml @@ -18,7 +18,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3 + 2.2.4-SNAPSHOT ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index cca527d5..6ab76b24 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3 + 2.2.4-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index 03e85556..313e5425 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3 + 2.2.4-SNAPSHOT ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index b548d6ea..afb7879d 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3 + 2.2.4-SNAPSHOT ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index 1e7e7d39..4dd062b9 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.2.3 + 2.2.4-SNAPSHOT database test diff --git a/simple/pom.xml b/simple/pom.xml index f23fb75a..24ca6b57 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3 + 2.2.4-SNAPSHOT ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index d55c0835..d65d8250 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.3 + 2.2.4-SNAPSHOT 4.0.0 @@ -24,7 +24,7 @@ org.linguafranca.pwdb util - 2.2.3 + 2.2.4-SNAPSHOT test diff --git a/util/pom.xml b/util/pom.xml index 9ef532aa..c5884357 100644 --- a/util/pom.xml +++ b/util/pom.xml @@ -6,7 +6,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.2.3 + 2.2.4-SNAPSHOT PWDB :: Util From daf94cd627c7f36f8f537165b6090a2d81482452 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sun, 5 Jan 2025 17:47:08 +0000 Subject: [PATCH 217/228] correcting changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1387a3d..230cbf59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.com) and [Semantic Versioning](http://semver.org/spec/v2.0.0.html) -## [2.2.3-SNAPSHOT] +## [2.2.3] 2025-01-05 ### Added From ad656e68290c78c6043ec7d118bd91643171b8a1 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Sun, 5 Jan 2025 21:23:01 +0000 Subject: [PATCH 218/228] Sort out some dependency clashes for Woodstox etc. --- jackson/pom.xml | 2 +- pom.xml | 2 +- simple/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jackson/pom.xml b/jackson/pom.xml index 70f3a02b..65e243d9 100644 --- a/jackson/pom.xml +++ b/jackson/pom.xml @@ -43,7 +43,7 @@ com.fasterxml.jackson.dataformat jackson-dataformat-xml - 2.17.1 + 2.18.2 com.fasterxml.woodstox diff --git a/pom.xml b/pom.xml index 4dd062b9..8e1074ff 100644 --- a/pom.xml +++ b/pom.xml @@ -147,7 +147,7 @@ org.apache.maven.plugins maven-dependency-plugin - 3.6.0 + 3.8.1 diff --git a/simple/pom.xml b/simple/pom.xml index 24ca6b57..76bc0a9f 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -54,7 +54,7 @@ com.fasterxml aalto-xml - 1.3.2 + 1.3.3 \ No newline at end of file From 578b4a0f389528f7ffe592df58e1d66aa9a78b6e Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 6 Jan 2025 11:42:24 +0000 Subject: [PATCH 219/228] Update readme.md --- readme.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/readme.md b/readme.md index 4c3dca47..731a7f0b 100644 --- a/readme.md +++ b/readme.md @@ -242,15 +242,15 @@ Each module corresponds to a Maven artifact. The GroupId is `org.linguafranca.pw databasedatabase -Javadocs +Javadocs Base definition of the Database APIs. -exampleexample -Javadocs +exampleexample +Javadocs Worked examples of loading, saving, splicing etc. using the APIs testtest -Javadocs +Javadocs Shared tests to assess the viability of the implementation. allKeePassJava2 @@ -258,31 +258,31 @@ Each module corresponds to a Maven artifact. The GroupId is `org.linguafranca.pw This is the main KeePassJava2 Maven dependency. Provides a route to all artifacts (other than test and examples) via transitive dependency. kdbKeePassJava2-kdb -Javadocs +Javadocs An implementation of the Database APIs supporting KeePass KDB format. kdbxKeePassJava2-kdbx -Javadocs +Javadocs Provides support for KDBX streaming and security. jacksonKeePassJava2-jackson -Javadocs +Javadocs A Jackson based implementation of KDBX. Intended to provide the main support going forward, replacing trhe modules below. -simpleKeePassJava2-simple -Javadocs +simpleKeePassJava2-simple +Javadocs A Simple XML Platform implementation of KDBX. Could be useful for Android. Simple cannot be used with Java versions 17 and up. -jaxbKeePassJava2-jaxb -Javadocs +jaxbKeePassJava2-jaxb +Javadocs A JAXB implementation of KDBX. Probably not useful for Android. The generated class bindings might be useful for building other interfaces. -domKeePassJava2-dom -Javadocs +domKeePassJava2-dom +Javadocs A DOM based implementation of KDBX. Being DOM based it is rather slow, but messes less with existing content than the other implementations. Known to work on Android. From c235d7808fd6ad8025745e052b2f9ed64f59cd17 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 6 Jan 2025 13:25:00 +0000 Subject: [PATCH 220/228] Update readme.md --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 731a7f0b..d1738ab0 100644 --- a/readme.md +++ b/readme.md @@ -110,7 +110,7 @@ Create credentials and an input stream for the password vault in question: then choose the Jackson based database implementation, and load the database - Database database = JacksonDatabase.load(credentials, inputStream) + Database database = JacksonDatabase.load(creds, inputStream) See [below](#database-implementations) for discussion of other database implementations. Note that they will not be maintained in the future - and see the following regarding making the storage of From 46434109c581d2be092645193bd537eaf18db49e Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 21 Jan 2025 11:51:04 +0000 Subject: [PATCH 221/228] Stop Jackson serialising empty elements --- .../org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java | 3 +++ .../pwdb/kdbx/jackson/JacksonSerializableDatabase.java | 2 ++ kdbx/src/main/resources/base.kdbx.xml | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java index d17615d5..fa8ed164 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java @@ -194,6 +194,9 @@ public boolean shouldProtect(String s) { } public List getBinaries() { + if (keePassFile.meta.binaries == null) { + keePassFile.createBinaries(); + } return keePassFile.meta.binaries; } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java index 356de849..254d005b 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java @@ -17,6 +17,7 @@ import com.ctc.wstx.api.WstxInputProperties; import com.ctc.wstx.api.WstxOutputProperties; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.SerializationFeature; @@ -98,6 +99,7 @@ public void save(OutputStream outputStream) { mapper.enable(ToXmlGenerator.Feature.WRITE_XML_DECLARATION); mapper.enable(SerializationFeature.INDENT_OUTPUT); mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); // set the serializer to Woodstox System.setProperty("javax.xml.stream.XMLOutputFactory", "com.ctc.wstx.stax.WstxOutputFactory"); diff --git a/kdbx/src/main/resources/base.kdbx.xml b/kdbx/src/main/resources/base.kdbx.xml index 5a791ae4..1958b946 100644 --- a/kdbx/src/main/resources/base.kdbx.xml +++ b/kdbx/src/main/resources/base.kdbx.xml @@ -16,7 +16,7 @@ - Keepass-Java-2 + KeePassJava2 New Database ${creationDate} From 08928c791a4f15a8a395a5ddd6e15ed94a88b74c Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 21 Jan 2025 11:51:04 +0000 Subject: [PATCH 222/228] Stop Jackson serialising empty elements #76 --- .../org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java | 3 +++ .../pwdb/kdbx/jackson/JacksonSerializableDatabase.java | 2 ++ kdbx/src/main/resources/base.kdbx.xml | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java index d17615d5..fa8ed164 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java @@ -194,6 +194,9 @@ public boolean shouldProtect(String s) { } public List getBinaries() { + if (keePassFile.meta.binaries == null) { + keePassFile.createBinaries(); + } return keePassFile.meta.binaries; } diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java index 356de849..254d005b 100644 --- a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java @@ -17,6 +17,7 @@ import com.ctc.wstx.api.WstxInputProperties; import com.ctc.wstx.api.WstxOutputProperties; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.SerializationFeature; @@ -98,6 +99,7 @@ public void save(OutputStream outputStream) { mapper.enable(ToXmlGenerator.Feature.WRITE_XML_DECLARATION); mapper.enable(SerializationFeature.INDENT_OUTPUT); mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); // set the serializer to Woodstox System.setProperty("javax.xml.stream.XMLOutputFactory", "com.ctc.wstx.stax.WstxOutputFactory"); diff --git a/kdbx/src/main/resources/base.kdbx.xml b/kdbx/src/main/resources/base.kdbx.xml index 5a791ae4..1958b946 100644 --- a/kdbx/src/main/resources/base.kdbx.xml +++ b/kdbx/src/main/resources/base.kdbx.xml @@ -16,7 +16,7 @@ - Keepass-Java-2 + KeePassJava2 New Database ${creationDate} From 611d24def00f9b2bfb4bd51b44f8b7334d809cb8 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Wed, 5 Mar 2025 20:00:08 +0000 Subject: [PATCH 223/228] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 230cbf59..e9c43189 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.com) and [Semantic Versioning](http://semver.org/spec/v2.0.0.html) +## [2.2.4] + +- [Issue 76, 78] Resolve incompatibility with KeePassXC (empty elements) +- [Issue 73] Trying to resolve dependency clashes for Woodstox etc + ## [2.2.3] 2025-01-05 ### Added From b9deec47747c39ac36c8190fd4d181643db53c2b Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Wed, 5 Mar 2025 20:24:19 +0000 Subject: [PATCH 224/228] Updating version to 2.2.4 --- all/pom.xml | 2 +- database/pom.xml | 2 +- dom/pom.xml | 2 +- example/pom.xml | 2 +- jackson/pom.xml | 2 +- jaxb/pom.xml | 2 +- kdb/pom.xml | 2 +- kdbx/pom.xml | 2 +- pom.xml | 4 ++-- readme.md | 14 +++++++------- simple/pom.xml | 2 +- test/pom.xml | 4 ++-- util/pom.xml | 2 +- 13 files changed, 21 insertions(+), 21 deletions(-) diff --git a/all/pom.xml b/all/pom.xml index cda4bc26..a3ab71cd 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.4-SNAPSHOT + 2.2.4 ../pom.xml diff --git a/database/pom.xml b/database/pom.xml index 457f32aa..62e75a5f 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.4-SNAPSHOT + 2.2.4 ../pom.xml 4.0.0 diff --git a/dom/pom.xml b/dom/pom.xml index 00b6beaa..18cdd161 100644 --- a/dom/pom.xml +++ b/dom/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.4-SNAPSHOT + 2.2.4 ../pom.xml 4.0.0 diff --git a/example/pom.xml b/example/pom.xml index e2334d12..fbe8c5ea 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.4-SNAPSHOT + 2.2.4 ../pom.xml 4.0.0 diff --git a/jackson/pom.xml b/jackson/pom.xml index 65e243d9..6a814b06 100644 --- a/jackson/pom.xml +++ b/jackson/pom.xml @@ -18,7 +18,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.4-SNAPSHOT + 2.2.4 ../pom.xml 4.0.0 diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 6ab76b24..d1dfa490 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.4-SNAPSHOT + 2.2.4 ../pom.xml 4.0.0 diff --git a/kdb/pom.xml b/kdb/pom.xml index 313e5425..18c1dc95 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.4-SNAPSHOT + 2.2.4 ../pom.xml 4.0.0 diff --git a/kdbx/pom.xml b/kdbx/pom.xml index afb7879d..c67cff19 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.4-SNAPSHOT + 2.2.4 ../pom.xml 4.0.0 diff --git a/pom.xml b/pom.xml index 8e1074ff..2d42c209 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.2.4-SNAPSHOT + 2.2.4 database test @@ -52,7 +52,7 @@ scm:git:git://github.com/jorabin/KeePassJava2.git scm:git:git@github.com:jorabin/KeePassJava2.git https://github.com/jorabin/KeePassJava2 - KeePassJava2-2.2.3 + KeePassJava2-2.2.4 diff --git a/readme.md b/readme.md index d1738ab0..d8983cc5 100644 --- a/readme.md +++ b/readme.md @@ -39,7 +39,7 @@ It is licensed under the Apache 2 License and is currently usable. ## Current Status -The current code is version 2.2.3 - released to Maven January 2025. This is on the main branch. See [Build from Source](#build-from-source) +The current code is version 2.2.4 - released to Maven March 2025. This is on the main branch. See [Build from Source](#build-from-source) Key updates relative to 2.1: - Java 8 (dependencies no longer support Java 7) @@ -54,11 +54,11 @@ See the [changelog](CHANGELOG.md) for more details. ### Release -The POM for the last release (2.2.3), Java 8 compatible, is +The POM for the last release (2.2.4), Java 8 compatible, is org.linguafranca.pwdb KeePassJava2-jackson - 2.2.3 + 2.2.4 at Maven Central. This provides access to the Jackson based implementation, which is now the recommended implementation. There is also a composite POM that @@ -66,18 +66,18 @@ provides access to all implementations (see [below](#database-implementations) f org.linguafranca.pwdb KeePassJava2 - 2.2.3 + 2.2.4 Note that the artifactId has become Camel Case from release 2.1.x onwards. ### Snapshot Snapshot builds are erratically available at [Sonatype](https://oss.sonatype.org/content/repositories/snapshots/org/linguafranca/pwdb/), next bug-fix release will be -2.2.4-SNAPSHOT (on branch `develop`) and work-in-progress 3.0.0-SNAPSHOT (on branch `v3`): +2.2.5-SNAPSHOT (on branch `develop`) and work-in-progress 3.0.0-SNAPSHOT (on branch `v3`): org.linguafranca.pwdb KeePassJava2 - 2.2.4-SNAPSHOT + 2.2.5-SNAPSHOT with appropriate `` entry, like: @@ -99,7 +99,7 @@ with appropriate `` entry, like: ## Java Version -From release 2.2 it requires Java 1.8. Earlier versions require Java 1.7. +From release 2.2 it requires Java 1.8. From release 3, Java 11. Earlier versions require Java 1.7. ## Quick Start diff --git a/simple/pom.xml b/simple/pom.xml index 76bc0a9f..4e06e62d 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.4-SNAPSHOT + 2.2.4 ../pom.xml 4.0.0 diff --git a/test/pom.xml b/test/pom.xml index d65d8250..8956bc93 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -3,7 +3,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.2.4-SNAPSHOT + 2.2.4 4.0.0 @@ -24,7 +24,7 @@ org.linguafranca.pwdb util - 2.2.4-SNAPSHOT + 2.2.4 test diff --git a/util/pom.xml b/util/pom.xml index c5884357..c3d3fb2d 100644 --- a/util/pom.xml +++ b/util/pom.xml @@ -6,7 +6,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.2.4-SNAPSHOT + 2.2.4 PWDB :: Util From 3f5136a9a5bafb26252c76a58147b541a29a2dfd Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Wed, 5 Mar 2025 22:05:25 +0000 Subject: [PATCH 225/228] Update readme.md --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index d8983cc5..cc231966 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent) [![javadoc](https://javadoc.io/badge2/org.linguafranca.pwdb/KeePassJava2/javadoc.svg)](https://javadoc.io/doc/org.linguafranca.pwdb/KeePassJava2) -![alt text](https://badgen.net/badge/Build/2.2.3/blue?icon=github) +![alt text](https://badgen.net/badge/Build/2.2.4/blue?icon=github) master [![CircleCI](https://dl.circleci.com/status-badge/img/gh/jorabin/KeePassJava2/tree/master.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/jorabin/KeePassJava2/tree/master) develop [![CircleCI](https://dl.circleci.com/status-badge/img/gh/jorabin/KeePassJava2/tree/develop.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/jorabin/KeePassJava2/tree/develop) @@ -321,7 +321,7 @@ Thanks to other contributors and raisers of issues. ## License -Copyright (c) 2024 Jo Rabin +Copyright (c) 2025 Jo Rabin Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From a858900ebe3c90e511e3a2d316786207bdb0e6fe Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Thu, 6 Mar 2025 11:25:37 +0000 Subject: [PATCH 226/228] Update Description --- all/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/all/pom.xml b/all/pom.xml index a3ab71cd..a621225b 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -27,7 +27,7 @@ KeePassJava2 KeePassJava2 :: All - KDB Database support as well as KDBX 3.1: DOM, JAXB and Simple implementations. + Supports KDBX Password Database Versions 3.1, 4 and 4.1. From 79249458a7f52a1336cd88609d963f03b31f4f92 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Mon, 23 Jun 2025 13:16:08 +0100 Subject: [PATCH 227/228] Missing date in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9c43189..37a2d598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.com) and [Semantic Versioning](http://semver.org/spec/v2.0.0.html) -## [2.2.4] +## [2.2.4] 2025-03-05 - [Issue 76, 78] Resolve incompatibility with KeePassXC (empty elements) - [Issue 73] Trying to resolve dependency clashes for Woodstox etc From b231847a9f72a9a294badbe1ce541b14dfb9f0d8 Mon Sep 17 00:00:00 2001 From: Jo Rabin Date: Tue, 17 Mar 2026 16:21:58 +0000 Subject: [PATCH 228/228] Update location of snapshot repo --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index cc231966..2461f909 100644 --- a/readme.md +++ b/readme.md @@ -84,7 +84,7 @@ with appropriate `` entry, like: oss.sonatype.org-snapshot - https://oss.sonatype.org/content/repositories/snapshots + https://central.sonatype.com/repository/maven-snapshots/ false