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
diff --git a/.gitignore b/.gitignore
index c601f37f..3ee52c53 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,13 @@
testOutput
-build.gradle
build
-settings.gradle
-gradlew
-gradlew.bat
-.gradle
-gradle
\ No newline at end of file
+.vscode/*
+*.versionsBackup
+**/target
+
+#settings.gradle
+#gradlew
+#gradlew.bat
+#.gradle
+#gradle
+#build.gradle
+
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 00000000..3d066ab9
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,3 @@
+language: java
+jdk: openjdk7
+script: mvn clean test
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 31e00bf1..37a2d598 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,100 @@
-#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.0] 2016-10-29
+
+## [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
+
+## [2.2.3] 2025-01-05
+
+### 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
+- [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
+
+### Added
+
+- 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
+
+- 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
+
+## [2.1.4] 2018-02-03
+
+### 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
+
+- Travis test failures relating to update to Openjdk 7 and a bug in Simple serialization
+
+### Added
+
+- Various functionality for searching databases
+- Recycle bin functionality
+- An experimental implementation of [keepasshttp](https://github.com/pfn/keepasshttp/) see
+[the readme](./readme.md) for warnings, limitations, etc. about this.
+
+## [2.1.2] 2018-01-20
+
+### Fixed
+
+- [Issue #16] Fix for split package
+
+## [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
+
+- Kdb Key File Support
+
+## [2.1.0] 2016-10-29
### Added
- Added a module structure to allow selective building, for android etc.
@@ -13,7 +106,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
@@ -33,7 +126,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/DatabaseStructure.svg b/DatabaseStructure.svg
new file mode 100644
index 00000000..0694e7aa
--- /dev/null
+++ b/DatabaseStructure.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
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 @@
-
-
\ No newline at end of file
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
diff --git a/KDBX.4.xsd b/KDBX.4.xsd
new file mode 100644
index 00000000..4fb7d70a
--- /dev/null
+++ b/KDBX.4.xsd
@@ -0,0 +1,560 @@
+
+
+
+
+
+ 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
+ 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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+
+
+
+ Abstract parent for StringField/Value so that
+ to allow creation of a custom mapping that contains extra
+ non-serialised (transient) fields.
+
+
+
+
+
+
+
+
+
+ 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
diff --git a/KdbxDiagram.svg b/KdbxDiagram.svg
new file mode 100644
index 00000000..fb278b1e
--- /dev/null
+++ b/KdbxDiagram.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/Module Structure.svg b/Module Structure.svg
deleted file mode 100644
index 7b271e9e..00000000
--- a/Module Structure.svg
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/Module Structure.xml b/Module Structure.xml
deleted file mode 100644
index 47b8708c..00000000
--- a/Module Structure.xml
+++ /dev/null
@@ -1 +0,0 @@
-7Vttb6M4EP41+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/ModuleStructure.svg b/ModuleStructure.svg
new file mode 100644
index 00000000..9579dffe
--- /dev/null
+++ b/ModuleStructure.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/PropertyValueProtection.md b/PropertyValueProtection.md
new file mode 100644
index 00000000..39e130ef
--- /dev/null
+++ b/PropertyValueProtection.md
@@ -0,0 +1,210 @@
+# 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 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.
+
+## 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 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,
+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 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. 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
+
+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
+
+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 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.
+
+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
+
+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();
+
+ 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 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. 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 {
+ /**
+ * A list of the properties that should be protected by default
+ */
+ List getProtectedProperties();
+
+ /**
+ * A factory for protected property values
+ */
+ PropertyValue.Factory extends PropertyValue> newProtected();
+
+ /**
+ * A factory for unprotected property values
+ */
+ PropertyValue.Factory extends PropertyValue> newUnprotected();
+
+ /**
+ * Return a factory given a property name and the properties that should be protected
+ */
+ default PropertyValue.Factory extends PropertyValue> 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.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 `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 `JacksonDatabase`.
+
+For other database implementations `Database.supportsPropertyValueStrategy()` returns false,
+and attempts to use any methods associated with PropertyValue
+cause an `UnsupportedOperationException` to be raised.
+
+
+
+
+
+
+
diff --git a/all/pom.xml b/all/pom.xml
index fb7b9719..a621225b 100644
--- a/all/pom.xml
+++ b/all/pom.xml
@@ -19,14 +19,15 @@
KeePassJava2-parentorg.linguafranca.pwdb
- 2.1.1-SNAPSHOT
+ 2.2.4../pom.xml
+
4.0.0KeePassJava2
- KeepassJava2 :: All
- KDB Database support as well as KDBX 3.1: DOM, JAXB and Simple implementations.
+ KeePassJava2 :: All
+ Supports KDBX Password Database Versions 3.1, 4 and 4.1.
@@ -49,5 +50,10 @@
KeePassJava2-simple${project.version}
+
+ org.linguafranca.pwdb
+ KeePassJava2-jackson
+ ${project.version}
+
\ No newline at end of file
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 {
+}
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/bitbucket-pipelines.yml b/bitbucket-pipelines.yml
new file mode 100644
index 00000000..a8c1de44
--- /dev/null
+++ b/bitbucket-pipelines.yml
@@ -0,0 +1,15 @@
+# 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:
+ #only on this branch
+ bitbucket-build:
+ - 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
diff --git a/database/pom.xml b/database/pom.xml
index bc035e29..62e75a5f 100644
--- a/database/pom.xml
+++ b/database/pom.xml
@@ -3,7 +3,7 @@
KeePassJava2-parentorg.linguafranca.pwdb
- 2.1.1-SNAPSHOT
+ 2.2.4../pom.xml4.0.0
@@ -16,13 +16,15 @@
com.google.guavaguava
- ${guava.version}
-
- com.madgag.spongycastle
- core
- ${spongycastle.core.version}
+ org.bouncycastle
+ bcpkix-jdk18on
+
+
+ org.linguafranca.pwdb
+ util
+ ${project.version}
\ No newline at end of file
diff --git a/database/src/main/java/org/linguafranca/pwdb/Credentials.java b/database/src/main/java/org/linguafranca/pwdb/Credentials.java
index 0ef1d817..7f03b64e 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,15 +33,13 @@ 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]);
}
}
- byte [] getKey();
+ /**
+ * Returns a digest of the composition of credentials supplied
+ */
+ byte[] getKey();
}
diff --git a/database/src/main/java/org/linguafranca/pwdb/Database.java b/database/src/main/java/org/linguafranca/pwdb/Database.java
index bed7ca7a..632f35ae 100644
--- a/database/src/main/java/org/linguafranca/pwdb/Database.java
+++ b/database/src/main/java/org/linguafranca/pwdb/Database.java
@@ -16,11 +16,12 @@
package org.linguafranca.pwdb;
-import org.linguafranca.pwdb.Credentials;
+import org.jetbrains.annotations.Nullable;
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.
@@ -31,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()},
@@ -76,7 +77,7 @@ public interface Database , G extends Group group);
/**
* Create a new Entry
@@ -112,19 +113,78 @@ 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
@@ -172,15 +232,69 @@ 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();
+
+ /**
+ * 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 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 propertyValueStrategy a propertyValue strategy
+ */
+ void setPropertyValueStrategy(PropertyValue.Strategy propertyValueStrategy);
+
+ /**
+ * returns true if the database supports non-standard property names
+ */
+ boolean supportsNonStandardPropertyNames();
+
+ /**
+ * returns true if the database supports binary properties
+ */
+ boolean supportsBinaryProperties();
+
+ /**
+ * returns true if the database supports recycle bin
+ */
+ boolean supportsRecycleBin();
+
+ /**
+ * returns true if the implementation supports a PropertyValueStrategy
+ */
+ boolean supportsPropertyValueStrategy();
+
}
diff --git a/database/src/main/java/org/linguafranca/pwdb/Entry.java b/database/src/main/java/org/linguafranca/pwdb/Entry.java
index a17cef26..2c2a95a3 100644
--- a/database/src/main/java/org/linguafranca/pwdb/Entry.java
+++ b/database/src/main/java/org/linguafranca/pwdb/Entry.java
@@ -16,6 +16,9 @@
package org.linguafranca.pwdb;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
import java.util.*;
/**
@@ -92,27 +95,67 @@ 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 #getPropertyValue(String)}
*
*
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()
*/
String getProperty(String name);
/**
- * Sets the value of a property.
+ * Gets the value of a property as a PropertyValue.
*
- *
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()
+ */
+ PropertyValue getPropertyValue(String name);
+
+ /**
+ * Sets the value of a property. Use of this method is not recommended for fields with protected values.
+ * Use {@link #setPropertyValue(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
* @throws UnsupportedOperationException if the name is not one of the standard properties and
- * non-standard properties are not supported.
+ * non-standard properties are not supported
+ * @see Database#supportsNonStandardPropertyNames()
*/
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 setPropertyValue(String name, PropertyValue 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
+ * @see Database#supportsNonStandardPropertyNames()
+ */
+ boolean removeProperty(String name) throws IllegalArgumentException, UnsupportedOperationException;
+
/**
* Returns a list of property names known to the entry.
*
@@ -129,6 +172,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);
@@ -139,25 +183,37 @@ 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);
+ /**
+ * 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 binary properties are not supported
+ * @see Database#supportsBinaryProperties()
+ */
+ boolean removeBinaryProperty(String name) throws UnsupportedOperationException;
+
/**
* Returns a list of binary property names known to the entry.
*
*
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();
/**
* Get the parent of this entry
- * @return a parent
+ * @return a parent or null if this entry currently does not have a parent
*/
- G getParent();
+ @Nullable G getParent();
/**
* Get the UUID of this entry. Databases (like KDB) that do not natively support
@@ -165,7 +221,7 @@ interface Matcher {
*
* @return a UUID
*/
- UUID getUuid();
+ @NotNull UUID getUuid();
/**
* Get the username field of this entry
@@ -193,6 +249,7 @@ interface Matcher {
*
* @return a password
*/
+ @Deprecated
String getPassword();
/**
@@ -201,7 +258,9 @@ interface Matcher {
*
Implementations should Touch LastModifiedTime when this method is called.
*
* @param pass a password
+ * @deprecated use {@link #setPropertyValue(String, PropertyValue)}
*/
+ @Deprecated
void setPassword(String pass);
/**
@@ -291,13 +350,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 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/Group.java b/database/src/main/java/org/linguafranca/pwdb/Group.java
index 2652ef15..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.
*
@@ -36,13 +43,18 @@ 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
@@ -132,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 extends E> findEntries(Entry.Matcher matcher, boolean recursive);
@@ -160,7 +176,7 @@ public interface Group , G extends Group parent);
@@ -171,15 +187,33 @@ public interface Group , G extends Group {
+ P of (CharSequence aCharSequence);
+
+ P of (char [] value);
+
+ P of (byte [] value);
+ }
+
+ /**
+ * A specification of which factories are to be used for unprotected values as opposed to protected values.
+ */
+ interface Strategy {
+ /**
+ * A list of the properties that should be protected
+ */
+ List getProtectedProperties();
+
+ /**
+ * A factory for protected properties
+ */
+ PropertyValue.Factory extends PropertyValue> newProtected();
+
+ /**
+ * A factory for unprotected property values
+ */
+ PropertyValue.Factory extends PropertyValue> newUnprotected();
+
+ /**
+ * Return a factory given a property name and the properties that should be protected
+ */
+ default PropertyValue.Factory extends PropertyValue> getFactoryFor(String propertyName) {
+ return getProtectedProperties().contains(propertyName) ?
+ newProtected() :
+ newUnprotected();
+ }
+
+ class Default implements Strategy {
+
+ @Override
+ public List getProtectedProperties() {
+ //noinspection ArraysAsListWithZeroOrOneArgument
+ return new ArrayList<>(Arrays.asList(Entry.STANDARD_PROPERTY_NAME_PASSWORD));
+ }
+
+ @Override
+ public Factory extends PropertyValue> newProtected() {
+ return SealedStore.getFactory();
+ }
+
+ @Override
+ public Factory extends PropertyValue> newUnprotected() {
+ return BytesStore.getFactory();
+ }
+ }
+ }
+
+ /**
+ * Values are stored as strings.
+ */
+ class StringStore implements PropertyValue {
+ private final String 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){
+ this.value = String.valueOf(aCharSequence);
+ }
+ public StringStore(char[] value){
+ this.value = String.valueOf(value);
+ }
+ public StringStore(byte[] value){
+ this.value = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(value)).toString();
+ }
+
+ @Override
+ public String getValueAsString() {
+ return value;
+ }
+
+ @Override
+ public char[] getValueAsChars() {
+ return value.toCharArray();
+ }
+
+ @Override
+ public byte[] getValueAsBytes() {
+ return value.getBytes(StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public CharSequence getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean isProtected() {
+ return false;
+ }
+ }
+
+ /**
+ * Property values are stored as byte arrays.
+ */
+ class BytesStore implements PropertyValue, Serializable {
+
+ private final byte[] value;
+
+ private final static PropertyValue.Factory factory =
+ new PropertyValue.Factory(){
+
+ @Override
+ public BytesStore of(CharSequence aCharSequence) {
+ return new BytesStore(aCharSequence);
+ }
+
+ @Override
+ public BytesStore of(char[] value) {
+ return new BytesStore(value);
+ }
+
+ @Override
+ public BytesStore of(byte[] value) {
+ return new BytesStore((value));
+ }
+ };
+
+ public static PropertyValue.Factory getFactory() {
+ return factory;
+ }
+ public BytesStore(CharSequence aString) {
+ this.value = charSequenceToBytes(aString);
+ }
+
+ public BytesStore(char [] value) {
+ this.value = charsToBytes(value);
+ }
+
+ public BytesStore(byte [] value) {
+ this.value = Arrays.copyOf(value, value.length);
+ }
+
+ @Override
+ public String getValueAsString() {
+ return new String(this.value);
+ }
+
+ @Override
+ public CharSequence getValue() {
+ ByteBuffer bb = ByteBuffer.wrap(this.value);
+ return Charsets.UTF_8.decode(bb);
+ }
+
+ @Override
+ public char [] getValueAsChars() {
+ return bytesToChars(this.value);
+ }
+
+ @Override
+ public byte [] getValueAsBytes() {
+ return Arrays.copyOf(this.value, this.value.length);
+ }
+
+ @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.
+ *
+ * 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 SecureRandom secureRandom = new SecureRandom();
+ private final SealedObject sealedObject;
+ private final ByteBuffer buffer;
+
+ 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 {
+ SecretKey key = retrieveKey(buffer);
+ byte[] cs = ((byte[]) sealedObject.getObject(key));
+ key.destroy();
+ return cs;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private final static PropertyValue.Factory factory =
+ new PropertyValue.Factory(){
+
+ @Override
+ public SealedStore of(CharSequence aCharSequence) {
+ return new SealedStore(aCharSequence);
+ }
+
+ @Override
+ public SealedStore of(char[] value) {
+ return new SealedStore(value);
+ }
+
+ @Override
+ public SealedStore of(byte[] value) {
+ return new SealedStore(value);
+ }
+ };
+
+ public static PropertyValue.Factory getFactory() {
+ return factory;
+ }
+
+ /**
+ * 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 {
+ private boolean destroyed = false;
+ private final byte[] key = new byte[16];
+ final String algorithm = "AES";
+
+ {
+ secureRandom.nextBytes(key);
+ }
+
+ @Override
+ public void destroy() {
+ Arrays.fill(this.key, (byte) 0);
+ this.destroyed = true;
+ }
+
+ @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();
+ }
+
+ @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){
+ try {
+ 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);
+ }
+ }
+
+ public SealedStore(char [] chars) {
+ this(charsToBytes(chars));
+ }
+
+ public SealedStore(CharSequence charSequence) {
+ this(charSequenceToBytes(charSequence));
+ }
+
+ @Override
+ public String getValueAsString() {
+ return new String(getBytes());
+ }
+
+ @Override
+ public char[] getValueAsChars() {
+ return bytesToChars(getBytes());
+ }
+
+ @Override
+ public byte[] getValueAsBytes() {
+ byte [] result = getBytes();
+ return Arrays.copyOf(result, result.length);
+ }
+
+ @Override
+ public CharSequence getValue() {
+ return bytesToCharSequence(getBytes());
+ }
+
+ @Override
+ public boolean isProtected() {
+ return true;
+ }
+ }
+}
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 69%
rename from kdbx/src/main/java/org/linguafranca/pwdb/kdbx/SerializableDatabase.java
rename to database/src/main/java/org/linguafranca/pwdb/SerializableDatabase.java
index a7ab2bf5..74e7a961 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,28 @@
* 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, 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 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 {
@@ -46,4 +50,10 @@ public interface SerializableDatabase {
byte[] getHeaderHash();
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/Visitor.java b/database/src/main/java/org/linguafranca/pwdb/Visitor.java
index ca3090d9..a7e3536b 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() {
@@ -67,17 +70,30 @@ 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 {
+ 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());
+ public void visit(E entry) {
+ printStream.println(entry.toString());
}
+
}
}
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 cd3e061a..e8cada25 100644
--- a/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java
+++ b/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java
@@ -18,7 +18,9 @@
import org.linguafranca.pwdb.*;
+import java.util.ArrayList;
import java.util.List;
+import java.util.UUID;
/**
* Base implementation of Database
@@ -39,14 +41,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()) {
@@ -61,7 +63,7 @@ public void visit(G group, Visitor visitor) {
}
if (!visitor.isEntriesFirst()) {
- for (Entry entry : group.getEntries()) {
+ for (E entry : group.getEntries()) {
visitor.visit(entry);
}
}
@@ -85,7 +87,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()));
@@ -126,4 +128,138 @@ public E newEntry(Entry,?,?,?> entry) {
// everything else should have been copied via properties
return result;
}
+
+ @Override
+ public E findEntry(final UUID uuid) {
+ List extends E> entries = findEntries(entry -> entry.getUuid().equals(uuid));
+ if (entries.size() > 1) {
+ throw new IllegalStateException("Two entries same UUID");
+ }
+ if (entries.isEmpty()) {
+ return null;
+ }
+ return entries.get(0);
+ }
+
+ @Override
+ public boolean deleteEntry(final UUID uuid) {
+ E e = findEntry(uuid);
+ if (e == null) {
+ return false;
+ }
+
+ //noinspection ConstantConditions
+ e.getParent().removeEntry(e);
+ if (isRecycleBinEnabled()) {
+ //noinspection ConstantConditions
+ getRecycleBin().addEntry(e);
+ }
+ return true;
+ }
+
+ @Override
+ public G findGroup(final UUID uuid){
+ final List groups = new ArrayList<>();
+ visit(new Visitor.Default() {
+ // set to true while visiting sub groups of recycle bin
+ boolean recycle;
+ @Override
+ public void startVisit(G group) {
+ if (!recycle && group.getUuid().equals(uuid)) {
+ groups.add(group);
+ }
+ if (group.isRecycleBin()) {
+ recycle = true;
+ }
+ }
+
+ @Override
+ public void endVisit(G group) {
+ if (group.isRecycleBin()) {
+ recycle = false;
+ }
+ }
+ });
+ if (groups.size() > 1) {
+ throw new IllegalStateException("Two groups same UUID");
+ }
+ if (groups.isEmpty()) {
+ return null;
+ }
+ return groups.get(0);
+ }
+
+ @Override
+ public boolean deleteGroup(final UUID uuid) {
+ G g = findGroup(uuid);
+ if (g==null) {
+ return false;
+ }
+
+ //noinspection ConstantConditions
+ g.getParent().removeGroup(g);
+ if (isRecycleBinEnabled()) {
+ //noinspection ConstantConditions
+ 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);
+ }
+ }
+
+ @Override
+ public boolean supportsNonStandardPropertyNames() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsBinaryProperties() {
+ return true;
+ }
+
+ @Override
+ 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 af5614fc..47f233d2 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
@@ -60,7 +57,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();
@@ -127,5 +124,15 @@ public void setNotes(String notes) {
touch();
}
+ @Override
+ public PropertyValue getPropertyValue(String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setPropertyValue(String name, PropertyValue value) {
+ throw new UnsupportedOperationException();
+ }
+
protected abstract void touch();
}
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..cf13c0af 100644
--- a/database/src/main/java/org/linguafranca/pwdb/base/AbstractGroup.java
+++ b/database/src/main/java/org/linguafranca/pwdb/base/AbstractGroup.java
@@ -45,18 +45,25 @@ public List extends G> findGroups(String group1) {
@Override
public List extends E> findEntries(String find, boolean recursive) {
- List result = new ArrayList<>(getEntries().size());
- for (E entry: getEntries()){
- if (entry.match(find)){
- result.add(entry);
+ /*
+ * 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 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;
+
+ return findEntries(new TextMatcher(find), recursive);
}
@Override
@@ -69,6 +76,10 @@ public List extends E> 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));
}
}
@@ -94,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/protect/ProtectedDatabase.java b/database/src/main/java/org/linguafranca/pwdb/protect/ProtectedDatabase.java
new file mode 100644
index 00000000..7c50e16c
--- /dev/null
+++ b/database/src/main/java/org/linguafranca/pwdb/protect/ProtectedDatabase.java
@@ -0,0 +1,58 @@
+package org.linguafranca.pwdb.protect;
+
+import org.linguafranca.pwdb.*;
+import org.linguafranca.pwdb.base.AbstractDatabase;
+
+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 PropertyValue.Strategy valueStrategy = new PropertyValue.Strategy.Default();
+
+ @Override
+ public boolean shouldProtect(String propertyName){
+ return valueStrategy.getProtectedProperties().contains(propertyName);
+ }
+
+ @Override
+ public void setShouldProtect(String propertyName, boolean protect){
+ if (protect) {
+ valueStrategy.getProtectedProperties().add(propertyName);
+ } else {
+ valueStrategy.getProtectedProperties().remove(propertyName);
+ }
+ }
+
+ @Override
+ public List listShouldProtect(){
+ return Collections.unmodifiableList(valueStrategy.getProtectedProperties());
+ }
+
+ /**
+ * 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/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..776cf39e
--- /dev/null
+++ b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java
@@ -0,0 +1,126 @@
+package org.linguafranca.pwdb.security;
+
+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;
+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
+ *
+ * The class is a singleton
+ */
+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 final String name = "AES";
+ private static final VariantDictionary kdfParameters = new VariantDictionary((short) 1);
+ static {
+ kdfParameters.putUuid("$UUID", KDF);
+ kdfParameters.putULong(ParamRounds, 6000L);
+ kdfParameters.putByteArray(ParamSeed, SecureRandom.getSeed(32));
+ }
+
+ /** 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]
+ }
+
+ /** 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 VariantDictionary createKdfParameters() {
+ return kdfParameters.copy();
+ }
+
+
+ @Override
+ 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);
+ 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());
+ }
+
+ /**
+ * Simplified version for KDBX V3
+ * @param key the 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));
+
+ // 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();
+ return md.digest(transformedKey);
+ }
+}
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..a0ba2984
--- /dev/null
+++ b/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java
@@ -0,0 +1,99 @@
+package org.linguafranca.pwdb.security;
+
+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.*;
+
+
+/**
+ * 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");
+
+ private static final String name = "Argon2";
+
+ /**
+ * 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 String getName() {
+ return name;
+ }
+
+ @Override
+ public byte[] getTransformedKey(byte[] digest, VariantDictionary argonParameterKeys) {
+ 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();
+
+ 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;
+ }
+
+ static final SecureRandom random = new SecureRandom();
+
+ @Override
+ public VariantDictionary createKdfParameters() {
+ VariantDictionary vd = new VariantDictionary((short) 1);
+ 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/ChaCha.java b/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java
new file mode 100644
index 00000000..65ad1f5b
--- /dev/null
+++ b/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java
@@ -0,0 +1,58 @@
+package org.linguafranca.pwdb.security;
+
+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;
+import java.util.UUID;
+
+/**
+ * ChaCha20 may be used for the inner stream as well as for the underlying database encryption scheme
+ *
+ * A singleton
+ */
+public class ChaCha implements CipherAlgorithm {
+
+ 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(){}
+ private static final ChaCha instance = new ChaCha();
+
+ public static ChaCha getInstance() {
+ return instance;
+ }
+
+ @Override
+ public UUID getCipherUuid() {
+ return CHA_CHA_20_CIPHER;
+ }
+
+ @Override
+ public String getName(){
+ return name;
+ }
+
+ @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);
+ }
+
+ @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..bb1a1c0a
--- /dev/null
+++ b/database/src/main/java/org/linguafranca/pwdb/security/CipherAlgorithm.java
@@ -0,0 +1,41 @@
+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();
+
+ /**
+ * Return the name of this algorithm
+ */
+ String getName();
+
+ /**
+ * Create a decrypted stream from the supplied encrypted one
+ *
+ * @param encryptedInputStream an encrypted 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 8c4f6ee8..36d4a46e 100644
--- a/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java
+++ b/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java
@@ -16,120 +16,225 @@
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.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 org.linguafranca.pwdb.StreamFormat;
+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
+ * Gets a SHA-256 message digest instance
*
- * @param string the string
- * @return a digest as a byte array
+ * @return A MessageDigest
*/
- @SuppressWarnings("unused")
- public static byte[] getDigest(String string) {
- return getDigest(string, "UTF-8");
+ public static MessageDigest getSha256MessageDigestInstance() {
+ try {
+ return MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("SHA-256 is not supported");
+ }
}
/**
- * Gets a digest for a string
+ * Gets a SHA-512 message digest instance
*
- * @param string the string
- * @param encoding the encoding of the String
- * @return a digest as a byte array
+ * @return A MessageDigest
*/
- 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 = getMessageDigestInstance();
-
+ public static MessageDigest getSha512MessageDigestInstance() {
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");
+ return MessageDigest.getInstance("SHA-512");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("SHA-512 is not supported");
}
}
/**
- * Gets a SHA-256 message digest instance
+ * Gets an HMacSha256 Mac
*
- * @return A MessageDigest
+ * @param key the key
+ * @return the Mac initialised with the key
*/
- public static MessageDigest getMessageDigestInstance() {
+ public static Mac getHMacSha256Instance(byte[] key) {
try {
- return MessageDigest.getInstance("SHA-256");
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalStateException("SHA-256 is not supported");
+ 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);
}
}
/**
- * Create a final key from the parameters passed
+ * 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 byte[] transformHmacKey(byte[] digest, byte[] transform) {
+ MessageDigest md = Encryption.getSha512MessageDigestInstance();
+ md.update(transform);
+ return md.digest(digest);
+ }
+
+ /**
+ * A list of functions that we can use to transform keys
+ * Enum constants forward to underlying implementation.
*/
- public static byte[] getFinalKeyDigest(byte[] key, byte[] masterSeed, byte[] transformSeed, long transformRounds) {
+ public enum KeyDerivationFunction implements org.linguafranca.pwdb.security.KeyDerivationFunction {
+ AES(Aes.getInstance()),
+ ARGON2(Argon2.getInstance());
+
+ private final org.linguafranca.pwdb.security.KeyDerivationFunction kdf;
+
+ KeyDerivationFunction(org.linguafranca.pwdb.security.KeyDerivationFunction kdf) {
+ this.kdf = kdf;
+ }
- AESEngine engine = new AESEngine();
- engine.init(true, new KeyParameter(transformSeed));
+ /**
+ * Find a KDF that matches this Uuid
+ *
+ * @param kdfUuid the Uuid to match
+ * @throws IllegalArgumentException if the Uuid is not known
+ */
+ 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;
+ }
+ }
+ throw new IllegalArgumentException("Unknown Cipher UUID");
+ }
- // copy input key
- byte[] transformedKey = new byte[key.length];
- System.arraycopy(key, 0, transformedKey, 0, transformedKey.length);
+ @Override
+ public UUID getKdfUuid() {
+ return kdf.getKdfUuid();
+ }
- // transform rounds times
- for (long rounds = 0; rounds < transformRounds; rounds++) {
- engine.processBlock(transformedKey, 0, transformedKey, 0);
- engine.processBlock(transformedKey, 16, transformedKey, 16);
+ @Override
+ public byte[] getTransformedKey(byte[] key, VariantDictionary transformParams) {
+ return kdf.getTransformedKey(key, transformParams);
}
- MessageDigest md = getMessageDigestInstance();
- byte[] transformedKeyDigest = md.digest(transformedKey);
+ @Override
+ public VariantDictionary createKdfParameters() {
+ return kdf.createKdfParameters();
+ }
- md.update(masterSeed);
- return md.digest(transformedKeyDigest);
+ @Override
+ public String getName(){
+ return kdf.getName();
+ }
}
/**
- * Create a decrypted input stream from an encrypted one
+ * A list of ciphers that we may apply to the database contents.
+ * Enum constants forward to underlying implementation.
*/
- public static InputStream getDecryptedInputStream (InputStream encryptedInputStream, 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);
+ public enum Cipher implements CipherAlgorithm {
+ CHA_CHA_20(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 String getName(){
+ return this.ef.getName();
+ }
+
+ @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);
+ }
}
/**
- * Create an encrypted output stream from an unencrypted output stream
+ * The ordinals represent various types of encryption that may
+ * be applied to fields within the unencrypted data
+ *
+ * @see StreamFormat
*/
- 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);
+ @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
new file mode 100644
index 00000000..0e15663f
--- /dev/null
+++ b/database/src/main/java/org/linguafranca/pwdb/security/KeyDerivationFunction.java
@@ -0,0 +1,32 @@
+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 function
+ *
+ * @param key the key to transform
+ * @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();
+
+ /**
+ * Get a name
+ */
+ String getName();
+}
diff --git a/database/src/main/java/org/linguafranca/pwdb/security/StreamEncryptor.java b/database/src/main/java/org/linguafranca/pwdb/security/StreamEncryptor.java
new file mode 100644
index 00000000..b3fc4a15
--- /dev/null
+++ b/database/src/main/java/org/linguafranca/pwdb/security/StreamEncryptor.java
@@ -0,0 +1,134 @@
+/*
+ * 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.security;
+
+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;
+
+/**
+ * KDBX "protected" fields are stream encrypted. They must be decrypted in
+ * the same order as they were encrypted.
+ *
+ * @author jo
+ */
+public interface StreamEncryptor {
+ byte[] getKey();
+
+ byte[] decrypt(byte[] encryptedText);
+
+ byte[] encrypt(byte[] decryptedText);
+
+ class None implements StreamEncryptor {
+
+ @Override
+ public byte[] getKey() {
+ return new byte[0];
+ }
+
+ @Override
+ public byte[] decrypt(byte[] encryptedText) {
+ return encryptedText;
+ }
+
+ @Override
+ 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 {
+ // 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) {
+ 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/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..0a95db47
--- /dev/null
+++ b/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java
@@ -0,0 +1,228 @@
+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.UUID;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.linguafranca.pwdb.security.VariantDictionary.EntryType.*;
+
+/**
+ * Implementation of a storage for V4 KDBX Header field parameters
+ *
+ * Though specific to KDBX V4 it is kept here for convenience of parameter passing to crypto functions
+ */
+
+@SuppressWarnings("WeakerAccess")
+public class VariantDictionary {
+
+ 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
+ */
+ @SuppressWarnings("unused")
+ public enum EntryType {
+ UINT32(0x4),
+ UINT64(0x5),
+ BOOL(0x8),
+ INT32(0xC),
+ INT64(0xD),
+ STRING(0x18), // UTF-8, without BOM, without null terminator
+ ARRAY(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 final byte type;
+ private final byte @NotNull [] value;
+ private final ByteOrder byteOrder;
+
+ public Entry(EntryType entryType, byte @NotNull [] value) {
+ this(entryType, value, ByteOrder.LITTLE_ENDIAN);
+ }
+
+ public Entry(EntryType entryType, byte @NotNull [] value, ByteOrder byteOrder) {
+ this.type = entryType.value;
+ this.value = checkNotNull(value, vnn);
+ this.byteOrder = byteOrder;
+ }
+
+ 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).getLong();
+ }
+
+ public int asInteger() {
+ if (value.length != 4) {
+ throw new IllegalStateException("Cannot convert value to int");
+ }
+ return ByteBuffer.wrap(value).order(byteOrder).getInt();
+ }
+
+ public byte @NotNull [] 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);
+ vd.entries.putAll(this.entries);
+ return vd;
+ }
+
+ /**
+ * get the entries in this dictionary
+ */
+ public Map getEntries(){
+ return entries;
+ }
+
+ /**
+ * Get the version number of this structure
+ *
+ * @return 1
+ */
+ public short getVersion() {
+ return version;
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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 @NotNull [] value) {
+ entries.put(checkNotNull(key), new Entry(type, checkNotNull(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(checkNotNull(key, knn), new Entry(ARRAY, buf));
+ }
+
+ /**
+ * Put a byte array under the key defined
+ */
+ public void putByteArray(@NotNull String key, byte @NotNull [] value) {
+ entries.put(checkNotNull(key, knn), new Entry(ARRAY, value));
+ }
+
+ /**
+ * Put a long as a 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(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];
+ ByteBuffer bb = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN);
+ 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/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..39e55a57
--- /dev/null
+++ b/database/src/test/java/org/linguafranca/pwdb/PropertyValueTest.java
@@ -0,0 +1,120 @@
+package org.linguafranca.pwdb;
+
+import org.junit.Test;
+
+import java.nio.charset.StandardCharsets;
+import org.linguafranca.test.util.TestUtil;
+
+import static org.junit.Assert.*;
+
+public class PropertyValueTest {
+
+ public static final String THIS_IS_A_SECRET = "This is a secret + לַחַיִּים";
+ public static final String ANOTHER_SECRET = "password with accents àéç";
+
+ static {
+ TestUtil.getTestPrintStream().println("I am only here for the dependency");
+ }
+
+ @Test
+ public void bytesTest() {
+ PropertyValue.BytesStore.Factory factory = PropertyValue.BytesStore.getFactory();
+
+ // test as CharSequence
+ 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());
+ 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(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());
+ assertArrayEquals(THIS_IS_A_SECRET.getBytes(StandardCharsets.UTF_8), testValue.getValueAsBytes());
+ }
+
+ @Test
+ public void sealedObjectTest() {
+ PropertyValue.SealedStore.Factory factory = PropertyValue.SealedStore.getFactory();
+
+ PropertyValue.SealedStore sealed = factory.of(THIS_IS_A_SECRET);
+ assertEquals(THIS_IS_A_SECRET, sealed.getValue().toString());
+ 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(StandardCharsets.UTF_8));
+ assertEquals(THIS_IS_A_SECRET, sealed.getValue().toString());
+ 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(StandardCharsets.UTF_8), 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.StringStore stringStore = factory.of(THIS_IS_A_SECRET);
+ assertEquals(THIS_IS_A_SECRET, stringStore.getValue());
+ 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(StandardCharsets.UTF_8));
+ assertEquals(THIS_IS_A_SECRET, stringStore.getValue());
+ 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(StandardCharsets.UTF_8), 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);
+ byte[] bytes = sealed.getValueAsBytes();
+ 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(StandardCharsets.UTF_8), 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
diff --git a/dom/pom.xml b/dom/pom.xml
index 74e18c0f..18cdd161 100644
--- a/dom/pom.xml
+++ b/dom/pom.xml
@@ -3,7 +3,7 @@
KeePassJava2-parentorg.linguafranca.pwdb
- 2.1.1-SNAPSHOT
+ 2.2.4../pom.xml4.0.0
@@ -21,14 +21,8 @@
org.linguafranca.pwdbtest${project.version}
+ test-jartest
-
- junit
- junit
- ${junit.version}
- test
-
-
-
+
\ No newline at end of file
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..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
@@ -16,10 +16,14 @@
package org.linguafranca.pwdb.kdbx.dom;
-import org.linguafranca.pwdb.base.AbstractDatabase;
-import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxStreamFormat;
-import org.linguafranca.pwdb.kdbx.StreamFormat;
+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.w3c.dom.Document;
import org.w3c.dom.Element;
@@ -28,32 +32,57 @@
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;
+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}.
*
* @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;
- private Element dbMeta;
- private DomSerializableDatabase domDatabase = DomSerializableDatabase.createEmptyDatabase();
+ private StreamFormat> streamFormat;
- public DomDatabaseWrapper () throws IOException {
+
+ 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 (Credentials credentials, InputStream inputStream) throws IOException {
- return new DomDatabaseWrapper(new KdbxStreamFormat(), credentials, inputStream);
+ /**
+ * 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"));
}
private void init() {
@@ -66,13 +95,30 @@ 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 {
+ DomHelper.getElement("//Generator", domDatabase.getDoc().getDocumentElement(), false).setTextContent("KeePassJava2-DOM");
streamFormat.save(domDatabase, credentials, outputStream);
setDirty(false);
}
@@ -82,7 +128,7 @@ public boolean shouldProtect(String name) {
if (protectionElement == null) {
return false;
}
- return Boolean.valueOf(protectionElement.getTextContent());
+ return Boolean.parseBoolean(protectionElement.getTextContent());
}
@Override
@@ -107,11 +153,49 @@ public DomIconWrapper newIcon() {
@Override
public DomIconWrapper newIcon(Integer i) {
- DomIconWrapper icon = newIcon();
+ DomIconWrapper icon = newIcon();
icon.setIndex(i);
return icon;
}
+ @Override
+ public DomGroupWrapper getRecycleBin() {
+ 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)) {
+ return g;
+ }
+ }
+ // the recycle bin seems to have been lost, better create another one
+ }
+ // uuid was 0 i.e. there isn't one
+ }
+ // no recycle bin group set up
+ if (!isRecycleBinEnabled()) {
+ return null;
+ }
+
+ DomGroupWrapper g = newGroup();
+ g.setName("Recycle Bin");
+ getRootGroup().addGroup(g);
+ ensureElementContent(RECYCLE_BIN_UUID_ELEMENT_NAME, dbMeta, base64FromUuid(g.getUuid()));
+ touchElement(RECYCLE_BIN_CHANGED_ELEMENT_NAME, dbMeta);
+ return g;
+ }
+
+ @Override
+ public boolean isRecycleBinEnabled() {
+ return Boolean.parseBoolean(getElementContent(RECYCLE_BIN_ENABLED_ELEMENT_NAME, dbMeta));
+ }
+
+ @Override
+ public void enableRecycleBin(boolean enable) {
+ setElementContent(RECYCLE_BIN_ENABLED_ELEMENT_NAME, dbMeta, ((Boolean) enable).toString());
+ }
+
public String getName() {
return DomHelper.getElementContent("DatabaseName", dbMeta);
}
@@ -133,4 +217,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/DomEntryWrapper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomEntryWrapper.java
index f13555bb..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.*;
/**
@@ -80,6 +79,14 @@ public void setProperty(String name, String value) {
database.setDirty(true);
}
+ @Override
+ 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);
+ return wasRemoved;
+ }
+
@Override
public List getPropertyNames() {
ArrayList result = new ArrayList<>();
@@ -112,6 +119,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<>();
@@ -158,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
@@ -180,27 +186,31 @@ 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 {
- 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 = 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/DomGroupWrapper.java b/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomGroupWrapper.java
index 64977a1b..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;
@@ -67,6 +68,15 @@ public boolean isRootGroup() {
return parent != null && (parent.getTagName().equals("Root"));
}
+ @Override
+ public boolean isRecycleBin() {
+ String UUIDcontent = getElementContent(RECYCLE_BIN_UUID_ELEMENT_NAME, database.dbMeta);
+ if (UUIDcontent != null){
+ UUID uuid = Helpers.uuidFromBase64(UUIDcontent);
+ return uuid.equals(this.getUuid());
+ }
+ return false;
+ }
@Override
public @Nullable DomGroupWrapper getParent() {
@@ -185,6 +195,7 @@ public void setIcon(DomIconWrapper icon) {
database.setDirty(true);
}
+ @NotNull
@Override
public DomDatabaseWrapper getDatabase() {
return database;
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..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
@@ -16,9 +16,10 @@
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.linguafranca.pwdb.kdbx.Helpers;
+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;
@@ -37,7 +38,8 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.security.SecureRandom;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
import java.util.Date;
/**
@@ -57,24 +59,21 @@ 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
- 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());
} catch (XPathExpressionException e) {
throw new IllegalStateException(e);
}
- result.setEncryption(new Salsa20StreamEncryptor(SecureRandom.getSeed(32)));
return result;
}
@@ -86,19 +85,19 @@ 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));
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");
+ element.setAttribute("kpj2-ProtectOnOutput", "True");
}
-
return this;
+
} catch (ParserConfigurationException e) {
throw new IllegalStateException("Instantiating Document Builder", e);
} catch (SAXException e) {
@@ -108,11 +107,27 @@ public SerializableDatabase load(InputStream inputStream) throws IOException {
}
}
+ @Override
+ public void addBinary(int index, byte[] payload) {
+ DomHelper.addBinary(doc.getDocumentElement(), Helpers.encodeBase64Content(payload, true),index);
+ }
+
+ @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) {
+ // 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 @@kpj2-ProtectOnOutput='True'
prepareProtection(copyDoc, "Title");
prepareProtection(copyDoc, "UserName");
prepareProtection(copyDoc, "Password");
@@ -120,9 +135,13 @@ public void save(OutputStream outputStream) {
prepareProtection(copyDoc, "URL");
// 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 = "";
@@ -133,19 +152,41 @@ 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;
+ if (time.equals("${creationDate}")) {
+ date = Date.from(Instant.now());
+ } else {
+ date = Helpers.toDate(time);
+ }
+ String encoded = Helpers.fromDate(date);
+ DomHelper.setElementContent(".", element, encoded);
+ }
+
} catch (XPathExpressionException e) {
throw new IllegalStateException(e);
}
Source xmlSource = new DOMSource(copyDoc);
- Result outputTarget = new StreamResult(outputStream);
try {
TransformerFactory factory = TransformerFactory.newInstance();
//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);
+ transformer.transform(xmlSource, new StreamResult(outputStream));
} catch (TransformerException e) {
throw new IllegalStateException(e);
}
@@ -156,7 +197,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
@@ -164,7 +205,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");
}
}
@@ -183,11 +224,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/DomEntryWrapperTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV3Test.java
similarity index 68%
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 0f4095b5..58ccdebc 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
@@ -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,9 +28,9 @@
/**
* @author jo
*/
-public class DomEntryWrapperTest extends EntryChecks {
+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);
}
@@ -39,4 +39,19 @@ public DomEntryWrapperTest () 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 Database newDatabase() {
+ return new DomDatabaseWrapper();
+ }
+
+ @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
new file mode 100644
index 00000000..97499e18
--- /dev/null
+++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomBinaryPropertyV4Test.java
@@ -0,0 +1,63 @@
+/*
+ * 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.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;
+import java.io.OutputStream;
+
+/**
+ * @author jo
+ */
+public class DomBinaryPropertyV4Test extends BinaryPropertyChecks {
+
+ public DomBinaryPropertyV4Test() throws IOException {
+ InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha20-Argon2-Attachment.kdbx");
+ database = DomDatabaseWrapper.load(new KdbxCreds("123".getBytes()), inputStream);
+ }
+
+ @Override
+ public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException {
+ 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 Database newDatabase() {
+ return new DomDatabaseWrapper();
+ }
+
+ @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/DomDatabaseWrapperTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomDatabaseWrapperTest.java
index a4de349e..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,27 +20,31 @@
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.StreamFormat;
+import org.linguafranca.pwdb.StreamFormat;
import org.linguafranca.pwdb.Credentials;
import java.io.IOException;
import java.io.InputStream;
+import java.io.PrintStream;
+
+import static org.linguafranca.test.util.TestUtil.getTestPrintStream;
/**
* @author jo
*/
public class DomDatabaseWrapperTest extends BasicDatabaseChecks {
+ static PrintStream printStream = getTestPrintStream();
+
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);
+ database.save(new StreamFormat.None(), new Credentials.None(), printStream);
}
@Test
@@ -48,9 +52,9 @@ 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);
+ 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..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
@@ -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.test.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/DomPropertyValueTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomPropertyValueTest.java
new file mode 100644
index 00000000..c695129a
--- /dev/null
+++ b/dom/src/test/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/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomRecycleBinTest.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomRecycleBinTest.java
new file mode 100644
index 00000000..9502ff6e
--- /dev/null
+++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomRecycleBinTest.java
@@ -0,0 +1,14 @@
+package org.linguafranca.pwdb.kdbx.dom;
+
+import org.linguafranca.pwdb.checks.RecycleBinChecks;
+
+import java.io.IOException;
+
+/**
+ * @author jo
+ */
+public class DomRecycleBinTest extends RecycleBinChecks {
+ public DomRecycleBinTest() throws IOException {
+ database = new DomDatabaseWrapper();
+ }
+}
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..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
@@ -16,11 +16,12 @@
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.kdbx.StreamFormat;
import org.linguafranca.pwdb.Credentials;
+import org.linguafranca.pwdb.kdbx.KdbxHeader;
import java.io.IOException;
import java.io.InputStream;
@@ -33,11 +34,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 {
@@ -59,4 +56,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/DomV4Test.java b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomV4Test.java
new file mode 100644
index 00000000..95345b3b
--- /dev/null
+++ b/dom/src/test/java/org/linguafranca/pwdb/kdbx/dom/DomV4Test.java
@@ -0,0 +1,34 @@
+package org.linguafranca.pwdb.kdbx.dom;
+
+import org.junit.Test;
+import org.linguafranca.pwdb.Visitor;
+import org.linguafranca.pwdb.kdbx.KdbxCreds;
+
+import java.io.InputStream;
+import java.io.PrintStream;
+
+import static org.linguafranca.test.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(printStream));
+ // test what happens to dates in V4
+ database.visit(new Visitor.Default(){
+
+ @Override
+ 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 43675e26..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
@@ -22,24 +22,29 @@
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.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertTrue;
+import static org.linguafranca.test.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);
}
};
@@ -64,7 +69,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/pom.xml b/example/pom.xml
index fa6af0e6..fbe8c5ea 100644
--- a/example/pom.xml
+++ b/example/pom.xml
@@ -19,7 +19,7 @@
KeePassJava2-parentorg.linguafranca.pwdb
- 2.1.1-SNAPSHOT
+ 2.2.4../pom.xml4.0.0
@@ -28,16 +28,26 @@
PWDB :: ExampleExamples of how to use PWDB
+
+ true
+
+
org.linguafranca.pwdbKeePassJava2${project.version}
+
+ io.github.novacrypto
+ SecureString
+ 2022.01.17
+ org.linguafranca.pwdbtest${project.version}
+ test-jar
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/ChooseFile.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java
new file mode 100644
index 00000000..6b376aac
--- /dev/null
+++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/ChooseFile.java
@@ -0,0 +1,79 @@
+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.*;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import static org.linguafranca.pwdb.kdbx.Util.streamToString;
+
+/**
+ * Utility to allow browsing of database files and listing content to console
+ */
+public class ChooseFile {
+
+ public static void main(String[] args) throws 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();
+ // 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 (Exception e) {
+ throw new RuntimeException(e);
+ }
+ fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
+ 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());
+ 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/OpenDbExample.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java
index 772d6a8e..e24f4e4f 100644
--- a/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java
+++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java
@@ -3,10 +3,14 @@
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;
import java.io.InputStream;
+import java.io.PrintStream;
+
+import static org.linguafranca.test.util.TestUtil.getTestPrintStream;
/**
* Example and naive speed test for various implementations.
@@ -15,6 +19,8 @@
*/
public class OpenDbExample {
+ static PrintStream printStream = getTestPrintStream();
+
private interface DbLoader {
Database load(KdbxCreds creds, InputStream inputStream) throws Exception;
}
@@ -40,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();
@@ -52,38 +65,42 @@ 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);
+ testDb(new JacksonDbLoader(), "Jackson", 5, 20);
- System.out.println("Sleeping");
+ printStream.println("Sleeping");
System.gc();
Thread.sleep(2000);
testDb(new SimpleDbLoader(), "Simple", 5, 20);
testDb(new JaxbDbLoader(), "Jaxb", 5, 20);
testDb(new DomDbLoader(), "Dom", 5, 20);
+ testDb(new JacksonDbLoader(), "Jackson", 5, 20);
- System.out.println("Sleeping");
+ printStream.println("Sleeping");
System.gc();
Thread.sleep(2000);
testDb(new SimpleDbLoader(), "Simple", 10, 1);
testDb(new JaxbDbLoader(), "Jaxb", 10, 1);
testDb(new DomDbLoader(), "Dom", 10, 1);
+ testDb(new JacksonDbLoader(), "Jackson", 10, 1);
- System.out.println("Sleeping");
+ printStream.println("Sleeping");
System.gc();
Thread.sleep(2000);
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/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java b/example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java
index ef1fc94e..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,19 +16,19 @@
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;
-import org.linguafranca.pwdb.Credentials;
+import org.linguafranca.pwdb.kdbx.dom.DomDatabaseWrapper;
+import org.linguafranca.pwdb.security.Encryption;
-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.linguafranca.test.util.TestUtil.getTestPrintStream;
+
/**
* Examples for QuickStart
*
@@ -37,10 +37,12 @@
@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);
- @BeforeClass
public static void ensureOutputDir() throws IOException {
Files.createDirectories(Paths.get("testOutput"));
}
@@ -56,7 +58,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));
}
}
@@ -99,17 +101,12 @@ public void splice(G newParent, Group, ?, ?, ?> groupToSplice) {
* Group by title
*/
public G groupByTitle(D database) {
- List extends E> entries = database.findEntries(new Entry.Matcher() {
- @Override
- public boolean matches(Entry entry) {
- return entry.getProperty(Entry.STANDARD_PROPERTY_NAME_TITLE).toLowerCase().contains("findme!");
- }
- });
+ List extends E> 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
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);
@@ -127,17 +124,75 @@ 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 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);
}
}
+
+ /**
+ * Load KDBX V3 save as KDBX V4
+ */
+ public void loadKdbx3SaveKdbx4(String resourceName, byte[] password, OutputStream v4OutputStream) 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);
+ }
+
+ // create a KDBX (database
+ D kdbxDatabase = getDatabase();
+ kdbxDatabase.setName("New 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
+ 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.KeyDerivationFunction.ARGON2);
+ kdbxHeader.setProtectedStreamAlgorithm(Encryption.ProtectedStreamAlgorithm.CHA_CHA_20);
+
+ // save it with format options
+ kdbxDatabase.save(formatV4, credentials, v4OutputStream);
+ }
+
+
+ /**
+ * 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);
+ }
+
+ // 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());
+
+ // 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);
+
+ // 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 7950442e..caa7ac8b 100644
--- a/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java
+++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/SaxParse.java
@@ -16,102 +16,79 @@
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.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
*/
public class SaxParse {
/**
- * SAX Parsing
+ * 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() 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();
+ public void exampleSaxParsing(String resourceName, Credentials credentials, PrintWriter writer) throws IOException, SAXException, ParserConfigurationException {
+ InputStream encryptedInputStream = getClass().getClassLoader().getResourceAsStream(resourceName);
+ 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 valueEncryptor = kdbxHeader.getInnerStreamEncryptor();
SAXParserFactory spfactory = SAXParserFactory.newInstance();
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("" + qName + ">");
+ public void endElement(String uri, String localName, String qName) {
+ writer.append("")
+ .append(qName)
+ .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(memoryProtection.decrypt(Helpers.decodeBase64Content(content.getBytes(), false)));
+ 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);
xmlReader.parse(xmlInputSource);
}
+ writer.flush();
}
}
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..cfffbe96
--- /dev/null
+++ b/example/src/main/java/org/linguafranca/pwdb/kdbx/Util.java
@@ -0,0 +1,91 @@
+package org.linguafranca.pwdb.kdbx;
+
+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());
+ }
+
+ public static InputStream getDecryptedInputStream (String resourceName, Credentials credentials, KdbxHeader header) throws IOException {
+ InputStream is = Util.class.getClassLoader().getResourceAsStream(resourceName);
+ return KdbxSerializer.createUnencryptedInputStream(credentials, header, is);
+ }
+
+ public static String streamToString(InputStream inputStream) throws IOException {
+ return CharStreams.toString(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+ }
+
+ /**
+ * 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 {
+ 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);
+ }
+
+
+ /**
+ * 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 credentials the credentials for the resource
+ * @param printWriter a PrintWriter to list the contents
+ */
+ public static void listXml(String resourceName, Credentials credentials, PrintWriter printWriter) throws IOException {
+ printWriter.println(streamToString(getDecryptedInputStream(resourceName, credentials)));
+ 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 1e2707ee..00000000
--- a/example/src/test/java/org/linguafranca/pwdb/example/SimpleQuickStartTest.java
+++ /dev/null
@@ -1,68 +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.Test;
-import org.linguafranca.pwdb.kdbx.QuickStart;
-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 SimpleDatabase getDatabase() {
- return new SimpleDatabase();
- }
-
- @Override
- public SimpleDatabase loadDatabase(Credentials credentials, InputStream inputStream){
- try {
- return SimpleDatabase.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();
- }
-
-
-}
diff --git a/example/src/test/java/org/linguafranca/pwdb/kdbx/SaxParseTest.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/SaxParseTest.java
new file mode 100644
index 00000000..741036f7
--- /dev/null
+++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/SaxParseTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.xml.sax.SAXException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+
+import static org.linguafranca.test.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", new KdbxCreds("123".getBytes()), new PrintWriter(outputStream));
+ }
+ @Test
+ 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/SimpleQuickStartTest.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/SimpleQuickStartTest.java
new file mode 100644
index 00000000..f9c8a71f
--- /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
+ 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());
+ 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
+ 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());
+ 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..8c88280b
--- /dev/null
+++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/UtilTest.java
@@ -0,0 +1,47 @@
+package org.linguafranca.pwdb.kdbx;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+
+import static org.linguafranca.pwdb.kdbx.Util.*;
+import static org.linguafranca.test.util.TestUtil.getTestPrintStream;
+
+public class UtilTest {
+
+ OutputStream outputStream = getTestPrintStream();
+ @Test public void listDatabaseTest() throws IOException {
+ listDatabase("V3-CustomIcon.kdbx", new KdbxCreds("123".getBytes()), outputStream);
+ }
+
+ @Test
+ public void listXmlTest() throws IOException {
+ PrintWriter writer = new PrintWriter(outputStream);
+ listXml("V3-CustomIcon.kdbx", new KdbxCreds("123".getBytes()), writer);
+ writer.flush();
+ }
+
+ /**
+ * List Database Encryption Characteristics
+ */
+ @Test
+ public void listKdbxHeaderParams () throws IOException {
+ PrintWriter writer = new PrintWriter(outputStream);
+ 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(outputStream);
+ listKdbxHeaderProperties("V4-AES-Argon2-CustomIcon.kdbx", writer);
+ listXml("V4-AES-Argon2-CustomIcon.kdbx", new KdbxCreds("123".getBytes()), writer);
+ writer.flush();
+ }
+}
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
new file mode 100644
index 00000000..55407ba4
--- /dev/null
+++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue27Test.java
@@ -0,0 +1,65 @@
+package org.linguafranca.pwdb.kdbx.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.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.linguafranca.test.util.TestUtil.getTestPrintStream;
+
+public class Issue27Test {
+ static PrintStream printStream = getTestPrintStream();
+ /**
+ * Check load of problem file
+ */
+ @Test
+ 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);
+ List extends JaxbEntry> entries = db.findEntries("testtitle");
+
+ for (JaxbEntry entry: entries) {
+ printStream.println(Helpers.fromDateV3(entry.getCreationTime()));
+ assertEquals("2021-01-11T09:18:56Z", Helpers.fromDateV3(entry.getCreationTime()));
+ }
+ }
+
+ /**
+ * Verify that V4 dates are still processed correctly
+ */
+ @Test
+ 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);
+ List extends JaxbEntry> entries = db.findEntries("Sample Entry #2 - Copy");
+
+ for (JaxbEntry entry: entries) {
+ printStream.println(Helpers.fromDate(entry.getCreationTime()));
+ assertEquals("2018-01-26T13:20:58Z", Helpers.fromDateV3(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);
+ printStream.println(CharStreams.toString(new InputStreamReader(plainText, StandardCharsets.UTF_8)));
+ }
+}
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..322ee10a
--- /dev/null
+++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue33Test.java
@@ -0,0 +1,69 @@
+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 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));
+ Helpers.isV4.set(true);
+ }
+
+ @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")));
+ }
+
+ @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/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue38Test.java b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue38Test.java
new file mode 100644
index 00000000..9cc87ac8
--- /dev/null
+++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue38Test.java
@@ -0,0 +1,33 @@
+package org.linguafranca.pwdb.kdbx.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;
+import java.io.PrintStream;
+import java.util.List;
+
+import static org.linguafranca.test.util.TestUtil.getTestPrintStream;
+
+/**
+ * Bug report on GitHub, the Keyfile is Version 2 (Hex)
+ */
+public class Issue38Test {
+
+ static PrintStream printStream = getTestPrintStream();
+ @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 extends DomEntryWrapper> entries = database.findEntries("Sample Entry");
+ DomEntryWrapper entry = entries.get(0);
+ printStream.println(entry.getTitle());
+ }
+}
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..65bfd42e
--- /dev/null
+++ b/example/src/test/java/org/linguafranca/pwdb/kdbx/validation/Issue70Test.java
@@ -0,0 +1,45 @@
+package org.linguafranca.pwdb.kdbx.validation;
+
+import org.junit.Test;
+import org.linguafranca.pwdb.kdbx.KdbxCreds;
+import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * 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 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)) {
+ JacksonDatabase database = JacksonDatabase.load(CREDENTIALS1, inputStream);
+ }
+ }
+
+ @Test
+ public void testFileFormat_4_1() throws IOException {
+ try (InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(TEST_RESOURCE2)){
+ 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/pom.xml b/jackson/pom.xml
new file mode 100644
index 00000000..6a814b06
--- /dev/null
+++ b/jackson/pom.xml
@@ -0,0 +1,54 @@
+
+
+
+
+ KeePassJava2-parent
+ org.linguafranca.pwdb
+ 2.2.4
+ ../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-jar
+ test
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-xml
+ 2.18.2
+
+
+ com.fasterxml.woodstox
+ woodstox-core
+ 7.0.0
+
+
+
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..fa8ed164
--- /dev/null
+++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonDatabase.java
@@ -0,0 +1,211 @@
+/*
+ * 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.Credentials;
+import org.linguafranca.pwdb.StreamConfiguration;
+import org.linguafranca.pwdb.StreamFormat;
+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;
+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;
+
+public class JacksonDatabase extends ProtectedDatabase {
+
+ KeePassFile keePassFile;
+ StreamFormat> streamFormat;
+
+ public JacksonDatabase() throws 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 {
+ 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-Jackson";
+ JacksonSerializableDatabase jacksonSerializableDatabase = new JacksonSerializableDatabase(this.keePassFile);
+ jacksonSerializableDatabase.setPropertyValueStrategy(this.getPropertyValueStrategy());
+ streamFormat.save(jacksonSerializableDatabase, 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() {
+ if (keePassFile.meta.binaries == null) {
+ keePassFile.createBinaries();
+ }
+ return keePassFile.meta.binaries;
+ }
+
+ 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
new file mode 100644
index 00000000..2f65db59
--- /dev/null
+++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonEntry.java
@@ -0,0 +1,362 @@
+/*
+ * 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.Icon;
+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;
+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 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;
+
+
+
+@JsonPropertyOrder({
+ "uuid",
+ "iconID",
+ "customIconUUID",
+ "foregroundColor",
+ "backgroundColor",
+ "overrideURL",
+ "previousParentGroup",
+ "tags",
+ "times",
+ "string",
+ "binary",
+ "autoType",
+ "customData",
+ "history",
+})
+
+
+@JsonIgnoreProperties({"path", "username", "title", "notes", "url", "password"})
+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")
+ @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 = "PreviousParentGroup")
+ @JsonDeserialize(converter = Base64ToUUIDConverter.class)
+ @JsonSerialize(converter = UUIDToBase64Converter.class)
+ protected UUID previousParentGroup;
+
+ @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;
+
+ @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 = "CustomData")
+ protected KeePassFile.CustomData customData;
+
+ @JacksonXmlProperty(localName = "History") /* Workaround jackson */
+ protected JacksonHistory history;
+
+ @Override
+ @JsonIgnore
+ public String getProperty(String s) {
+ StringProperty sp = getStringProperty(s, string);
+ if (sp == null) {
+ return null;
+ }
+ return sp.getValue().getValueAsString();
+ }
+
+ @Override
+ @JsonIgnore
+ public void setProperty(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;
+ }
+ string.add(new StringProperty(name, value));
+ touch();
+ }
+
+ @Override
+ @JsonIgnore
+ 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
+ @JsonIgnore
+ 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
+ @JsonIgnore
+ public boolean removeBinaryProperty(String name) throws UnsupportedOperationException {
+ BinaryProperty bp = getBinaryProp(name, binary);
+ if (bp != null) {
+ binary.remove(bp);
+ touch();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ @JsonIgnore
+ 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
+ @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");
+ 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
new file mode 100644
index 00000000..b98278fb
--- /dev/null
+++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonGroup.java
@@ -0,0 +1,301 @@
+/*
+ * 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 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 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.KeePassFile;
+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",
+"name",
+"notes",
+"iconID",
+"customIconUUID",
+"times",
+"isExpanded",
+"defaultAutoTypeSequence",
+"enableAutoType",
+"enableSearching",
+"lastTopVisibleEntry",
+"previousParentGroup",
+"tags",
+"customData",
+"entry",
+"group",
+})
+@JsonIgnoreProperties(ignoreUnknown=true)
+public class JacksonGroup extends 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 = "PreviousParentGroup")
+ @JsonDeserialize(converter = Base64ToUUIDConverter.class)
+ @JsonSerialize(converter = UUIDToBase64Converter.class)
+ protected UUID previousParentGroup;
+
+ @JacksonXmlProperty(localName = "Tags")
+ protected String tags;
+
+ @JacksonXmlProperty(localName = "CustomData")
+ protected KeePassFile.CustomData customData;
+
+ @JacksonXmlProperty(localName = "Entry") /* Workaround jackson */
+ @JacksonXmlElementWrapper(useWrapping = false)
+ protected List entries;
+
+
+ @JacksonXmlProperty(localName = "Group") /* Workaround jackson */
+ @JacksonXmlElementWrapper(useWrapping = false)
+ protected List groups;
+
+ @JsonIgnore
+ protected JacksonDatabase database;
+
+ @JsonIgnore
+ protected JacksonGroup parent;
+
+ public JacksonGroup() {
+ entries = new ArrayList<>();
+ groups = 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 g) {
+ JacksonGroup group = (JacksonGroup) g;
+ 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() {
+ return new ArrayList<>(groups);
+ }
+
+ @Override
+ @JsonIgnore
+ public int getGroupsCount() {
+ return groups.size();
+ }
+
+ @Override
+ public JacksonGroup addGroup(JacksonGroup g) {
+ JacksonGroup group = (JacksonGroup) g;
+ 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.groups.add(group);
+ touch();
+ return group;
+ }
+
+ @Override
+ public JacksonGroup removeGroup(JacksonGroup g) {
+ JacksonGroup group = (JacksonGroup) g;
+ if (this.database != group.database) {
+ throw new IllegalStateException("Must be from same database");
+ }
+ this.groups.remove(group);
+ group.parent = null;
+ touch();
+ return group;
+ }
+
+ @Override
+ public List getEntries() {
+ return new ArrayList<>(this.entries);
+ }
+
+ @Override
+ @JsonIgnore
+ public int getEntriesCount() {
+ return this.entries.size();
+ }
+
+ @Override
+ public JacksonEntry addEntry(JacksonEntry e) {
+ JacksonEntry entry = (JacksonEntry) e;
+ if (this.database != entry.database) {
+ throw new IllegalStateException("Must be from same database");
+ }
+ if (entry.getParent() != null) {
+ entry.getParent().removeEntry(entry);
+ }
+ this.entries.add(entry);
+ entry.parent = this;
+ touch();
+ return entry;
+ }
+
+ @Override
+ public JacksonEntry removeEntry(JacksonEntry e) {
+ JacksonEntry entry = (JacksonEntry) e;
+ if (this.database != entry.database) {
+ throw new IllegalStateException("Must be from same database");
+ }
+ this.entries.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..c9ea238c
--- /dev/null
+++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonHistory.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+public class JacksonHistory {
+
+ @JacksonXmlProperty(localName = "Entry") /* Workaround jackson */
+ @JacksonXmlElementWrapper(useWrapping = false)
+ private List entry;
+
+ public JacksonHistory() {
+ entry = new ArrayList<>();
+ }
+
+ /**
+ * @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..7e3cb732
--- /dev/null
+++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonIcon.java
@@ -0,0 +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 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..254d005b
--- /dev/null
+++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/JacksonSerializableDatabase.java
@@ -0,0 +1,209 @@
+/*
+ * 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 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;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;
+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.KeePassFile;
+import org.linguafranca.pwdb.security.StreamEncryptor;
+
+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.Objects;
+
+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;
+
+ private PropertyValue.Strategy propertyValueStrategy = new PropertyValue.Strategy.Default();
+
+ public static KeePassFile createEmptyDatabase() throws IOException {
+
+ InputStream inputStream = JacksonSerializableDatabase.class.getClassLoader()
+ .getResourceAsStream("base.kdbx.xml");
+ XmlMapper mapper = new XmlMapper();
+ return mapper.readValue(inputStream, KeePassFile.class);
+
+ }
+
+ public JacksonSerializableDatabase() {
+ }
+
+ public JacksonSerializableDatabase(KeePassFile keePassFile) {
+ this.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);
+ keePassFile = mapper.readValue(inputStream, KeePassFile.class);
+ return this;
+ }
+
+
+ @Override
+ public void save(OutputStream outputStream) {
+ try {
+ SimpleModule module = new SimpleModule();
+ module.addSerializer(PropertyValue.class, new ValueSerializer(encryptor, propertyValueStrategy));
+ // 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);
+ mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
+
+ // 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);
+ try {
+ sw.setPrefix("xml", "http://www.w3.org/XML/1998/namespace");
+
+ mapper.writeValue(sw, keePassFile);
+
+ sw.writeEndDocument();
+ } finally {
+ sw.close();
+ osw.close();
+ }
+
+ } catch(Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @Override
+ public byte[] getHeaderHash() {
+ return keePassFile.meta.headerHash;
+ }
+
+ @Override
+ public void setHeaderHash(byte[] hash) {
+ this.keePassFile.meta.headerHash = hash;
+ }
+
+
+ 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);
+ }
+
+ // 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);
+ String value = binary.getValue();
+ return Helpers.decodeBase64Content(value.getBytes(), binary.getCompressed());
+ }
+
+ @Override
+ public int 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.groups) {
+ group.parent = parent;
+ group.database = parent.database;
+ fixUp(group);
+ }
+
+ 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);
+ }
+
+ 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/Base64ToByteConverter.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToByteConverter.java
new file mode 100644
index 00000000..66eb416c
--- /dev/null
+++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToByteConverter.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 java.util.Base64;
+
+import com.fasterxml.jackson.databind.util.StdConverter;
+
+public class Base64ToByteConverter extends StdConverter {
+
+ @Override
+ public byte[] convert(String value) {
+ return Base64.getDecoder().decode(value);
+ }
+}
diff --git a/simple/src/main/java/org/linguafranca/xml/XmlEventTransformer.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToUUIDConverter.java
similarity index 53%
rename from simple/src/main/java/org/linguafranca/xml/XmlEventTransformer.java
rename to jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToUUIDConverter.java
index 787c00bd..b2b07d29 100644
--- a/simple/src/main/java/org/linguafranca/xml/XmlEventTransformer.java
+++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/Base64ToUUIDConverter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2015 Jo Rabin
+ * 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.
@@ -14,24 +14,22 @@
* limitations under the License.
*/
-package org.linguafranca.xml;
+package org.linguafranca.pwdb.kdbx.jackson.converter;
-import javax.xml.stream.events.XMLEvent;
+import java.util.UUID;
-/**
- * An interface for allowing XML events to be transformed by {@link XmlOutputStreamFilter}
- * and {@link XmlInputStreamFilter}.
- *
- * @author jo
- */
-public interface XmlEventTransformer {
- XMLEvent transform(XMLEvent event);
+import org.linguafranca.pwdb.kdbx.Helpers;
+
+import com.fasterxml.jackson.databind.util.StdConverter;
+
+public class Base64ToUUIDConverter extends StdConverter {
- @SuppressWarnings("unused")
- class None implements XmlEventTransformer {
- @Override
- public XMLEvent transform(XMLEvent event) {
- return event;
+ @Override
+ public UUID convert(String value) {
+ if(value == null || value.isEmpty()) {
+ return UUID.randomUUID();
}
+ return Helpers.uuidFromBase64(value);
}
-}
\ No newline at end of file
+
+}
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
new file mode 100644
index 00000000..fd613a9f
--- /dev/null
+++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ByteToBase64Converter.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 java.util.Base64;
+
+import com.fasterxml.jackson.databind.util.StdConverter;
+
+public class ByteToBase64Converter extends StdConverter {
+
+ @Override
+ public String convert(byte[] value) {
+ 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..c215f90e
--- /dev/null
+++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/StringToDateConverter.java
@@ -0,0 +1,38 @@
+/*
+ * 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 java.util.Objects;
+
+import org.linguafranca.pwdb.kdbx.Helpers;
+
+import com.fasterxml.jackson.databind.util.StdConverter;
+
+public class StringToDateConverter extends StdConverter {
+
+ @Override
+ public Date convert(String value) {
+ // 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;
+ }
+ 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..3564185e
--- /dev/null
+++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/UUIDToBase64Converter.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 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) {
+ return Helpers.base64FromUuid(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
new file mode 100644
index 00000000..3c9639b4
--- /dev/null
+++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueDeserializer.java
@@ -0,0 +1,71 @@
+/*
+ * 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 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.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;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+public class ValueDeserializer extends StdDeserializer {
+
+ private final StreamEncryptor encryptor;
+ private final PropertyValue.Strategy strategy;
+
+ public ValueDeserializer(StreamEncryptor encryptor, PropertyValue.Strategy strategy) {
+ super(ValueDeserializer.class);
+ this.encryptor = encryptor;
+ this.strategy = strategy;
+ }
+
+ @Override
+ public PropertyValue deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+
+ JsonNode node = p.getCodec().readTree(p);
+
+ if (node.isTextual()) {
+ 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
+ String cipherText = "";
+ if (node.has("")) {
+ cipherText = 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);
+ }
+
+ 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
new file mode 100644
index 00000000..3e25b25c
--- /dev/null
+++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/converter/ValueSerializer.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.kdbx.jackson.converter;
+
+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.PropertyValue;
+import org.linguafranca.pwdb.security.StreamEncryptor;
+
+import java.io.IOException;
+
+
+public class ValueSerializer extends StdSerializer {
+
+ private final StreamEncryptor encryptor;
+ private final PropertyValue.Strategy propertyValueStrategy;
+
+ public ValueSerializer(StreamEncryptor encryptor, PropertyValue.Strategy propertyValueStrategy) {
+ super(ValueSerializer.class, false);
+ this.encryptor = encryptor;
+ this.propertyValueStrategy = propertyValueStrategy;
+ }
+
+ private String encrypt(byte[] bytes) {
+ //Cipher
+ byte[] encrypted = encryptor.encrypt(bytes);
+ //Convert to base64
+ return new String(Base64.encodeBase64(encrypted));
+ }
+
+ @Override
+ public void serialize(PropertyValue value, JsonGenerator gen, SerializerProvider provider) throws IOException {
+
+ final ToXmlGenerator xmlGenerator = (ToXmlGenerator) gen;
+ xmlGenerator.writeStartObject();
+
+ String stringToWrite = value.isProtected() ?
+ encrypt(value.getValueAsBytes()) :
+ value.getValueAsString();
+
+ if (value.isProtected()) {
+ xmlGenerator.setNextIsAttribute(true);
+ xmlGenerator.writeStringField("Protected", "True");
+ }
+ xmlGenerator.setNextIsAttribute(false);
+ xmlGenerator.setNextIsUnwrapped(true);
+ xmlGenerator.writeStringField("text", stringToWrite);
+ xmlGenerator.writeEndObject();
+ }
+}
\ No newline at end of file
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..c768d4eb
--- /dev/null
+++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/EntryClasses.java
@@ -0,0 +1,158 @@
+/*
+ * 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 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;
+
+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;
+
+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 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;
+ }
+
+ public static class AutoType {
+ @JacksonXmlProperty(localName = "Enabled")
+ @JsonDeserialize(converter = StringToBooleanConverter.class)
+ @JsonSerialize(converter = BooleanToStringConverter.class)
+ 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