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 @@ + + + +
Save/Load
Save/Load
SerializableDatabase
(Jackson, DOM, JAXB, Simple)
SerializableDatabase...
Encrypt/Decrypt
Encrypt/Decrypt
StreamFormat
(Kdbx, None)
StreamFormat...
Read/Write Values
Read/Write Values
Save/Load
Save/Load
Database Implementation
(Memory Model, DOM etc.)
Database Implementation...
Save/Load
Save/Load
User Read/Write
User Read/Write
Database Facade
(Jackson, DOM Wrapper, JAXB, SImple)
Database Facade...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/Format Diagram.svg b/Format Diagram.svg deleted file mode 100644 index 86b27bea..00000000 --- a/Format Diagram.svg +++ /dev/null @@ -1,2 +0,0 @@ - -
Hashed Blocks
Hashed Blocks
Magic Number
Magic Number
Version
Version
Header
Header
Header
Header
Header
Header
Header 0
Header 0
...
...
Value of Header 9
Value of Header 9
0
0
8
8
4
4
Type
Type
Length
Length
Value
Value
Encrypted
Encrypted
Unencrypted
Unencrypted
Sequence
Sequence
MD5 Hash
MD5 Hash
Length
Length
KDBX 3.1 File
KDBX 3.1 File
Header Format
Header Format
Hashed Block
Format
Hashed Block<div>Format</div>
Unencrypted
Payload
(Optionally Compressed)
[Not supported by viewer]
36
36
0
0
40
40
12
12
1
1
3
3
0
0
Byte
Offset
[Not supported by viewer]
Byte
Offset
[Not supported by viewer]
Byte
Offset
[Not supported by viewer]
Hashed Blocks
Hashed Blocks
Magic Number
Magic Number
Version
Version
Header
Header
Header
Header
Header
Header
Header 0
Header 0
...
...
0
0
8
8
32
32
Type
Type
Length
Length
Value
Value
Unencrypted
Unencrypted
Length
Length
Header Format
Header Format
Hashed Block
Format
Hashed Block<div>Format</div>
HMAC-SHA-256
HMAC-SHA-256
36
36
0
0
12
12
1
1
5
5
0
0
Byte
Offset
[Not supported by viewer]
Byte
Offset
[Not supported by viewer]
Byte
Offset
[Not supported by viewer]
KDBX 4 File
KDBX 4 File
http://keepass.info/help/kb/kdbx_4.html
http://keepass.info/help/kb/kdbx_4.html
HMAC-SHA-256
HMAC-SHA-256
Encrypted
Payload
(Optionally Compressed)
[Not supported by viewer]
\ No newline at end of file diff --git a/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 @@ + + + +
Magic Number
Magic Number
Version
Version
Header
Header
Header
Header
Header
Header
Header 0
Header 0
...
...
Value of Header 9
Value of Header 9
0
0
8
8
4
4
Type
Type
Length
Length
Value
Value
Encrypted
Encryp...
Unencrypted
Unencr...
Sequence
Sequence
MD5 Hash
MD5 Hash
Length
Length
KDBX 3.1 File
KDBX 3.1 File
Header Format
Header Format
Hashed Block
Format
Hashed Block...
Unencrypted
Payload
(Optionally Compressed)
Unencrypted...
36
36
0
0
40
40
12
12
1
1
3
3
0
0
Byte
Offset
Byte...
Byte
Offset
Byte...
Byte
Offset
Byte...
Magic Number
Magic Number
Version
Version
Header
Header
Header
Header
Header
Header
Header 0
Header 0
...
...
0
0
8
8
32
32
Type
Type
Length
Length
Value
Value
Unencrypted
Unencr...
Length
Length
Header Format
Header Format
Hashed Block
Format
Hashed Block...
HMAC-SHA-256
HMAC-SHA-256
36
36
0
0
12
12
1
1
5
5
0
0
Byte
Offset
Byte...
Byte
Offset
Byte...
Byte
Offset
Byte...
KDBX 4 File
KDBX 4 File
As described at http://keepass.info/...
SHA-256
SHA-256
Encrypted
Payload
(Optionally Compressed)
Encrypted...
HMAC-SHA-256
HMAC-SHA-256
Inner Header
Inner Header
Hashed Blocks
Hashed Blocks
Inner Header
Inner Header
...
...
Header 0
Header 0
XML Database
XML Database
Hashed Blocks
Hashed Blocks
XML Database
XML Database
Copyright (c) 2023 Jo Rabin, Licensed under the Apache2 license. See KeePassJava2.
Copyright (c) 2023 Jo Rabin, Licensed under the Apache2 license. See KeePassJava2.
Text is not SVG - cannot display
\ No newline at end of file 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 @@ - -
all
all
simple
simple
jaxb
jaxb
dom
dom
kdb
kdb
kdbx
kdbx
database
database
Test
Test
example
example
Other, future
Other, future
KDBX Implementations
KDBX Implementations
KDB Implementation
KDB Implementation
???
???
Implementatiions
of the 
org.linguafranca.pwdb.Database Interface
[Not supported by viewer]
\ No newline at end of file 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 @@ + + + +
all
all
simple
simple
jaxb
jaxb
dom
dom
kdb
kdb
kdbx
kdbx
database
database
Test
Test
example
example
Other, future
Other,...
KDBX Implementations
KDBX I...
KDB Implementation
KDB Im...
???
???
Implementatiions
of the 
org.linguafranca.pwdb.Database Interface
Implem...
jackson
jackson
Text is not SVG - cannot display
\ No newline at end of file diff --git a/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 newProtected(); + + /** + * A factory for unprotected property values + */ + PropertyValue.Factory newUnprotected(); + + /** + * Return a factory given a property name and the properties that should be protected + */ + default PropertyValue.Factory getFactoryFor(String propertyName) { + return getProtectedProperties().contains(propertyName) ? + newProtected() : + newUnprotected(); + } +} +``` + +#### Default Implementations of PropertyValue +There are three default implementations of `PropertyValue`: +- `PropertyValue.StringStore` stores values as strings +- `PropertyValue.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-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.2.4 ../pom.xml + 4.0.0 KeePassJava2 - 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-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.2.4 ../pom.xml 4.0.0 @@ -16,13 +16,15 @@ com.google.guava guava - ${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 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 newProtected(); + + /** + * A factory for unprotected property values + */ + PropertyValue.Factory newUnprotected(); + + /** + * Return a factory given a property name and the properties that should be protected + */ + default PropertyValue.Factory getFactoryFor(String propertyName) { + return getProtectedProperties().contains(propertyName) ? + newProtected() : + newUnprotected(); + } + + class Default implements Strategy { + + @Override + public List getProtectedProperties() { + //noinspection ArraysAsListWithZeroOrOneArgument + return new ArrayList<>(Arrays.asList(Entry.STANDARD_PROPERTY_NAME_PASSWORD)); + } + + @Override + public Factory newProtected() { + return SealedStore.getFactory(); + } + + @Override + public Factory 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 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 findGroups(String group1) { @Override public List 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 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-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.2.4 ../pom.xml 4.0.0 @@ -21,14 +21,8 @@ org.linguafranca.pwdb test ${project.version} + test-jar test - - 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-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.2.4 ../pom.xml 4.0.0 @@ -28,16 +28,26 @@ PWDB :: Example Examples of how to use PWDB + + true + + org.linguafranca.pwdb KeePassJava2 ${project.version} + + io.github.novacrypto + SecureString + 2022.01.17 + org.linguafranca.pwdb test ${project.version} + test-jar diff --git a/example/src/main/java/org/linguafranca/pwdb/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 entries = database.findEntries(new Entry.Matcher() { - @Override - public boolean matches(Entry entry) { - return entry.getProperty(Entry.STANDARD_PROPERTY_NAME_TITLE).toLowerCase().contains("findme!"); - } - }); + List entries = database.findEntries(entry -> entry.getProperty(Entry.STANDARD_PROPERTY_NAME_TITLE).toLowerCase().contains("findme!")); // create a new group using DB factory method G newParent = database.newGroup("Found entries"); // iterate over the found entries 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(""); + public void endElement(String uri, String localName, String qName) { + writer.append(""); } @Override - public void characters(char[] ch, int start, int length) throws SAXException { + public void characters(char[] ch, int start, int length) { String content = new String(ch, start, length); if (protectedContent) { - content = new String(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 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 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 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 windowAndKeystrokeSequence; + } + } + + public static class StringProperty { + + @JacksonXmlProperty(localName = "Key") + String key; + + @JacksonXmlProperty(localName = "Value") + PropertyValue value; + + @SuppressWarnings("unused") + public StringProperty() {} + + public StringProperty(String key, PropertyValue value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public PropertyValue getValue() { + return value; + } + + public void setKey(String key) { + this.key = key; + } + + public void setValue(PropertyValue value) { + this.value = value; + } + } + + public static class BinaryProperty { + + @JacksonXmlProperty(localName = "Key") + String key; + + @JacksonXmlProperty(localName = "Value") + Value value; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public void setValue(Value value) { + this.value = value; + } + + public static class Value { + @JacksonXmlProperty(localName = "Ref", isAttribute = true) + String ref; + + public void setRef(String ref) { + this.ref = ref; + } + } + } + + public static class History { + + @JacksonXmlProperty(localName = "Entry") /* Workaround jackson */ + @JacksonXmlElementWrapper(useWrapping = false) + private List list; + + public History() { + list = new ArrayList<>(); + } + + public List getHistory() { + return list; + } + } +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java new file mode 100644 index 00000000..fa591f66 --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/KeePassFile.java @@ -0,0 +1,374 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx.jackson.model; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.linguafranca.pwdb.Entry; +import org.linguafranca.pwdb.kdbx.jackson.JacksonGroup; +import org.linguafranca.pwdb.kdbx.jackson.converter.Base64ToByteConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.Base64ToUUIDConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.BooleanToStringConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.ByteToBase64Converter; +import org.linguafranca.pwdb.kdbx.jackson.converter.DateToStringConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.StringToBooleanConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.StringToDateConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.UUIDToBase64Converter; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText; + +@JacksonXmlRootElement(localName = "KeePassFile") +public class KeePassFile { + + @JacksonXmlProperty(localName = "Meta") + public Meta meta; + @JacksonXmlProperty(localName = "Root") + public Root root; + + public void createBinaries() { + meta.binaries = new ArrayList<>(); + } + + @JsonPropertyOrder({ + "group", + "deletedObject" + }) + public static class Root { + + public Root() { + } + + @JacksonXmlProperty(localName = "Group") + public JacksonGroup group; + + @JacksonXmlProperty(localName = "DeletedObjects") + protected Object deletedObject; + + } + + @JsonPropertyOrder({ + "generator", + "headerHash", + "databaseName", + "databaseNameChanged", + "databaseDescription", + "databaseDescriptionChanged", + "defaultUserName", + "defaultUserNameChanged", + "maintenanceHistoryDays", + "color", + "masterKeyChanged", + "masterKeyChangeRec", + "masterKeyChangeForce", + "masterKeyChangeForceOnce", + "memoryProtection", + "customIcons", + "recycleBinEnabled", + "recycleBinUUID", + "recycleBinChanged", + "entryTemplatesGroup", + "entryTemplatesGroupChanged", + "lastSelectedGroup", + "lastTopVisibleGroup", + "historyMaxItems", + "historyMaxSize", + "binaries", + }) + public static class Meta { + + public Meta() { + } + + @JacksonXmlProperty(localName = "Generator") + public String generator; + + @JacksonXmlProperty(localName = "HeaderHash") + @JsonDeserialize(converter = Base64ToByteConverter.class) + @JsonSerialize(converter = ByteToBase64Converter.class) + public byte[] headerHash; + + @JacksonXmlProperty(localName = "DatabaseName") + public String databaseName; + + @JacksonXmlProperty(localName = "DatabaseNameChanged") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + public Date databaseNameChanged; + + @JacksonXmlProperty(localName = "DatabaseDescription") + public String databaseDescription; + + @JacksonXmlProperty(localName = "DatabaseDescriptionChanged") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + public Date databaseDescriptionChanged; + + @JacksonXmlProperty(localName = "DefaultUserName") + protected String defaultUserName; + + @JacksonXmlProperty(localName = "DefaultUserNameChanged") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + protected Date defaultUserNameChanged; + + @JacksonXmlProperty(localName = "MaintenanceHistoryDays") + protected int maintenanceHistoryDays; + + @JacksonXmlProperty(localName = "Color") + protected String color; + + @JacksonXmlProperty(localName = "MasterKeyChanged") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + protected Date masterKeyChanged; + + @JacksonXmlProperty(localName = "MasterKeyChangeRec") + protected int masterKeyChangeRec; + + @JacksonXmlProperty(localName = "MasterKeyChangeForce") + protected int masterKeyChangeForce; + + @JacksonXmlProperty(localName = "MasterKeyChangeForceOnce") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) + protected Boolean masterKeyChangeForceOnce; + + @JacksonXmlProperty(localName = "MemoryProtection") + public KeePassFile.MemoryProtection memoryProtection; + + @JacksonXmlElementWrapper(localName = "CustomIcons") + protected ArrayList customIcons; + + @JacksonXmlProperty(localName = "RecycleBinEnabled") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) + public Boolean recycleBinEnabled; + + @JacksonXmlProperty(localName = "RecycleBinUUID") + @JsonDeserialize(converter = Base64ToUUIDConverter.class) + @JsonSerialize(converter = UUIDToBase64Converter.class) + public UUID recycleBinUUID; + + @JacksonXmlProperty(localName = "RecycleBinChanged") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + public Date recycleBinChanged; + + @JacksonXmlProperty(localName = "EntryTemplatesGroup") + @JsonDeserialize(converter = Base64ToUUIDConverter.class) + @JsonSerialize(converter = UUIDToBase64Converter.class) + protected UUID entryTemplatesGroup; + + @JacksonXmlProperty(localName = "EntryTemplatesGroupChanged") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + protected Date entryTemplatesGroupChanged; + + @JacksonXmlProperty(localName = "LastSelectedGroup") + @JsonDeserialize(converter = Base64ToUUIDConverter.class) + @JsonSerialize(converter = UUIDToBase64Converter.class) + protected UUID lastSelectedGroup; + + @JacksonXmlProperty(localName = "LastTopVisibleGroup") + @JsonDeserialize(converter = Base64ToUUIDConverter.class) + @JsonSerialize(converter = UUIDToBase64Converter.class) + protected UUID lastTopVisibleGroup; + + @JacksonXmlProperty(localName = "HistoryMaxItems") + protected int historyMaxItems; + + @JacksonXmlProperty(localName = "HistoryMaxSize") + protected int historyMaxSize; + + @JacksonXmlProperty(localName = "Binaries") + public List binaries; + + @JacksonXmlProperty(localName = "CustomData") + protected KeePassFile.CustomData customData; + + /* version 4 */ + + @JacksonXmlProperty(localName = "SettingsChanged") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + protected Date settingsChanged; + } + + @JsonPropertyOrder({ + "protectTitle", + "protectUserName", + "protectPassword", + "protectURL", + "protectNotes" + }) + public static class MemoryProtection { + public MemoryProtection() { + } + + @JacksonXmlProperty(localName = "ProtectTitle") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) + protected Boolean protectTitle; + + @JacksonXmlProperty(localName = "ProtectUserName") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) + protected Boolean protectUserName; + + @JacksonXmlProperty(localName = "ProtectPassword") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) + protected Boolean protectPassword; + + @JacksonXmlProperty(localName = "ProtectURL") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) + protected Boolean protectURL; + + @JacksonXmlProperty(localName = "ProtectNotes") + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) + protected Boolean protectNotes; + + public Boolean shouldProtect(String name) { + switch (name) { + case Entry.STANDARD_PROPERTY_NAME_TITLE: + return protectTitle; + case Entry.STANDARD_PROPERTY_NAME_USER_NAME: + return protectUserName; + case Entry.STANDARD_PROPERTY_NAME_PASSWORD: + return protectPassword; + case Entry.STANDARD_PROPERTY_NAME_URL: + return protectURL; + case Entry.STANDARD_PROPERTY_NAME_NOTES: + return protectNotes; + } + return false; + } + } + + public static class Binaries { + public Binaries() { + } + } + + public static class Binary { + + public Binary() { + } + + @JacksonXmlText + protected String value; + + @JacksonXmlProperty(localName = "ID", isAttribute = true) + protected Integer id; + + @JacksonXmlProperty(localName = "Compressed", isAttribute = true) + @JsonDeserialize(converter = StringToBooleanConverter.class) + @JsonSerialize(converter = BooleanToStringConverter.class) + protected Boolean compressed; + + public String getValue() { + return value; + } + + public Boolean getCompressed() { + return compressed; + } + + public Integer getId() { + return id; + } + + public void setId(Integer Id) { + this.id = Id; + } + + public void setValue(String value) { + this.value = value; + } + + public void setCompressed(boolean compressed) { + this.compressed = compressed; + } + } + + public static class Icon { + public Icon() { + } + + @JacksonXmlProperty(localName = "UUID") + @JsonDeserialize(converter = Base64ToUUIDConverter.class) + @JsonSerialize(converter = UUIDToBase64Converter.class) + protected UUID uuid; + + @JacksonXmlProperty(localName = "Name") + protected String name; + + @JacksonXmlProperty(localName = "LastModificationTime") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + protected Date lastModificationTime; + + @JacksonXmlProperty(localName = "Data") + @JsonDeserialize(converter = Base64ToByteConverter.class) + @JsonSerialize(converter = ByteToBase64Converter.class) + protected byte[] data; + + public String getName() { + return uuid.toString(); + } + } + + public static class CustomData { + + public static class CustomDataItem { + @JacksonXmlProperty(localName = "Key") + public String key; + @JacksonXmlProperty(localName = "Value") + public String value; + @JacksonXmlProperty(localName = "LastModificationTime") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + public Date lastModificationTime; + } + + @JacksonXmlProperty(localName = "Item") + @JacksonXmlElementWrapper(useWrapping = false) + protected List items; + + public CustomData() { + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + } +} diff --git a/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/Times.java b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/Times.java new file mode 100644 index 00000000..0e1f8ba2 --- /dev/null +++ b/jackson/src/main/java/org/linguafranca/pwdb/kdbx/jackson/model/Times.java @@ -0,0 +1,129 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx.jackson.model; + +import java.util.Date; + +import org.linguafranca.pwdb.kdbx.jackson.converter.DateToStringConverter; +import org.linguafranca.pwdb.kdbx.jackson.converter.StringToDateConverter; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; + +public class Times { + @JacksonXmlProperty(localName = "LastModificationTime") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + protected Date lastModificationTime; + + @JacksonXmlProperty(localName = "CreationTime") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + protected Date creationTime; + + @JacksonXmlProperty(localName = "LastAccessTime") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + protected Date lastAccessTime; + + @JacksonXmlProperty(localName = "ExpiryTime") + @JsonSerialize(converter = DateToStringConverter.class) + @JsonDeserialize(converter = StringToDateConverter.class) + protected Date expiryTime; + + @JacksonXmlProperty(localName = "Expires") + protected Boolean expires; + + @JacksonXmlProperty(localName = "UsageCount") + protected int usageCount; + + @JacksonXmlProperty(localName = "LocationChanged") + @JsonDeserialize(converter = StringToDateConverter.class) + @JsonSerialize(converter = DateToStringConverter.class) + protected Date locationChanged; + + public Date getLastModificationTime() { + return lastModificationTime; + } + + public void setLastModificationTime(Date lastModificationTime) { + this.lastModificationTime = lastModificationTime; + } + + public Date getCreationTime() { + return creationTime; + } + + public void setCreationTime(Date creationTime) { + this.creationTime = creationTime; + } + + public Date getLastAccessTime() { + return lastAccessTime; + } + + public void setLastAccessTime(Date lastAccessTime) { + this.lastAccessTime = lastAccessTime; + } + + public Date getExpiryTime() { + return expiryTime; + } + + public void setExpiryTime(Date expiryTime) { + this.expiryTime = expiryTime; + } + + public Boolean getExpires() { + return expires; + } + + public void setExpires(Boolean expires) { + this.expires = expires; + } + + public int getUsageCount() { + return usageCount; + } + + public void setUsageCount(int usageCount) { + this.usageCount = usageCount; + } + + public Date getLocationChanged() { + return locationChanged; + } + + public void setLocationChanged(Date locationChanged) { + this.locationChanged = locationChanged; + } + + public Times() { + this(new Date(System.currentTimeMillis() / 1000 * 1000)); + } + + public Times(Date date) { + lastModificationTime = date; + lastAccessTime = date; + locationChanged = date; + creationTime = date; + expiryTime = date; + expires = false; + usageCount = 0; + } +} diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV3Test.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV3Test.java new file mode 100644 index 00000000..41c14567 --- /dev/null +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV3Test.java @@ -0,0 +1,66 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.linguafranca.pwdb.checks.BinaryPropertyChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; + +public class JacksonBinaryPropertyV3Test extends BinaryPropertyChecks { + + + public JacksonBinaryPropertyV3Test() { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment.kdbx"); + try { + database = JacksonDatabase.load(new KdbxCreds("123".getBytes()),inputStream); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + @Override + public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { + database.save(credentials, outputStream); + } + + @Override + public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + return JacksonDatabase.load(credentials, inputStream); + } + + @Override + public Database newDatabase() { + Database result = null; + try { + result = new JacksonDatabase(); + } catch(Exception e) { + throw new RuntimeException(e); + } + return result; + + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } + +} diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV4Test.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV4Test.java new file mode 100644 index 00000000..ec65bdc4 --- /dev/null +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonBinaryPropertyV4Test.java @@ -0,0 +1,66 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.linguafranca.pwdb.checks.BinaryPropertyChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; + +public class JacksonBinaryPropertyV4Test extends BinaryPropertyChecks{ + + + public JacksonBinaryPropertyV4Test() throws Exception{ + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha20-Argon2-Attachment.kdbx"); + database = JacksonDatabase.load(new KdbxCreds("123".getBytes()), inputStream); + } + + + @Override + public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { + database.save(credentials, outputStream); + } + + @Override + public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + try { + return JacksonDatabase.load(credentials, inputStream); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public Database newDatabase() { + Database result = null; + try { + result = new JacksonDatabase(); + } catch (Exception e) { + throw new RuntimeException(e); + } + return result; + + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } + +} diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoadTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoadTest.java new file mode 100644 index 00000000..06e40d78 --- /dev/null +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoadTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb; + +import java.io.InputStream; +import java.io.PrintStream; + +import org.junit.Test; +import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; + +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; + +public class JacksonDatabaseLoadTest { + + static PrintStream printStream = getTestPrintStream(); + + + @Test + public void loadXml() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("ExampleDatabase.xml"); + JacksonDatabase database = JacksonDatabase.loadXml(inputStream); + database.visit(new Visitor.Print(printStream)); + } + @Test + public void loadKdbx() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test123.kdbx"); + JacksonDatabase database = JacksonDatabase.load(new KdbxCreds("123".getBytes()), inputStream); + database.visit(new Visitor.Print(printStream)); + } + + @Test + public void loadKdbxV4() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); + JacksonDatabase database = JacksonDatabase.load(new KdbxCreds("123".getBytes()), inputStream); + database.visit(new Visitor.Print(printStream)); + // test what happens to dates in V4 + database.visit(new Visitor.Default(){ + @Override + public void visit(Entry entry) { + printStream.println(entry.getCreationTime()); + } + }); + } + + @Test + public void emptyDb() throws Exception { + JacksonDatabase database = new JacksonDatabase(); + printStream.println(database.getDescription()); + } + + @Test + public void dbWithDeleted() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("testDeleted.kdbx"); + JacksonDatabase database = JacksonDatabase.load(new KdbxCreds("123".getBytes()), inputStream); + database.visit(new Visitor.Print(printStream)); + } + +} diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoaderTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoaderTest.java new file mode 100644 index 00000000..7c218fc3 --- /dev/null +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseLoaderTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb; + +import java.io.InputStream; + +import org.linguafranca.pwdb.checks.DatabaseLoaderChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; + +public class JacksonDatabaseLoaderTest extends DatabaseLoaderChecks{ + + + public JacksonDatabaseLoaderTest() throws Exception{ + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test123.kdbx"); + // file has password credentials + Credentials credentials = new KdbxCreds("123".getBytes()); + super.database = JacksonDatabase.load(credentials, inputStream); + } + +} diff --git a/example/src/test/java/org/linguafranca/pwdb/example/SaxParseTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseTest.java similarity index 58% rename from example/src/test/java/org/linguafranca/pwdb/example/SaxParseTest.java rename to jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseTest.java index 74d660a7..cbaaa291 100644 --- a/example/src/test/java/org/linguafranca/pwdb/example/SaxParseTest.java +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonDatabaseTest.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. @@ -13,24 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.linguafranca.pwdb; -package org.linguafranca.pwdb.example; +import java.io.IOException; -import org.junit.Test; -import org.linguafranca.pwdb.kdbx.SaxParse; -import org.xml.sax.SAXException; +import org.linguafranca.pwdb.checks.BasicDatabaseChecks; +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; -import javax.xml.parsers.ParserConfigurationException; -import java.io.IOException; +public class JacksonDatabaseTest extends BasicDatabaseChecks { -/** - * @author jo - */ -public class SaxParseTest extends SaxParse { + public JacksonDatabaseTest() throws IOException { - @Test - public void exampleSaxparsing () throws IOException, SAXException, ParserConfigurationException { - super.exampleSaxparsing(); } + + @Override + public Database createDatabase() throws IOException { + return new JacksonDatabase(); + } + } diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java new file mode 100644 index 00000000..d7e29b0d --- /dev/null +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonPropertyValueTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2024 Jo Rabin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb; + +import org.linguafranca.pwdb.checks.BasicDatabaseChecks; +import org.linguafranca.pwdb.checks.PropertyValueChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; +import org.linguafranca.pwdb.kdbx.jackson.JacksonEntry; +import org.linguafranca.pwdb.kdbx.jackson.JacksonGroup; +import org.linguafranca.pwdb.kdbx.jackson.JacksonIcon; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class JacksonPropertyValueTest extends PropertyValueChecks { + + public JacksonPropertyValueTest() throws IOException { + super(true); + } + + @Override + public void saveDatabase(JacksonDatabase database, Credentials credentials, OutputStream outputStream) throws IOException { + database.save(credentials, outputStream); + } + + @Override + public JacksonDatabase loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + return JacksonDatabase.load(credentials, inputStream); + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } + @Override + public JacksonDatabase createDatabase() throws IOException { + return new JacksonDatabase(); + } + +} diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonRecycleBinTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonRecycleBinTest.java new file mode 100644 index 00000000..fc63749d --- /dev/null +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonRecycleBinTest.java @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb; + +import org.linguafranca.pwdb.checks.RecycleBinChecks; +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; + +public class JacksonRecycleBinTest extends RecycleBinChecks { + + public JacksonRecycleBinTest() { + try { + database = new JacksonDatabase(); + } catch(Exception e) { + e.printStackTrace(); + } + + } +} diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonSaveAndReloadTest.java b/jackson/src/test/java/org/linguafranca/pwdb/JacksonSaveAndReloadTest.java new file mode 100644 index 00000000..8e09395e --- /dev/null +++ b/jackson/src/test/java/org/linguafranca/pwdb/JacksonSaveAndReloadTest.java @@ -0,0 +1,79 @@ +/* + * Copyright 2023 Giuseppe Valente + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb; + +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.linguafranca.pwdb.checks.SaveAndReloadChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.KdbxHeader; + +import org.linguafranca.pwdb.kdbx.jackson.JacksonDatabase; +import org.linguafranca.pwdb.kdbx.jackson.JacksonEntry; +import org.linguafranca.pwdb.kdbx.jackson.JacksonGroup; +import org.linguafranca.pwdb.kdbx.jackson.JacksonIcon; + + +public class JacksonSaveAndReloadTest extends SaveAndReloadChecks{ + + @Override + public JacksonDatabase getDatabase() { + try { + return new JacksonDatabase(); + } catch(Exception e) { + return null; + } + + } + + @Override + public JacksonDatabase getDatabase(String name, Credentials credentials) throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(name); + return JacksonDatabase.load(credentials, inputStream); + } + + @Override + public void saveDatabase(JacksonDatabase database, Credentials credentials, OutputStream outputStream) throws IOException { + database.save(credentials, outputStream); + } + + @Override + public JacksonDatabase loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + return JacksonDatabase.load(credentials, inputStream); + } + + + @Override + public boolean verifyStreamFormat(StreamFormat s1, StreamFormat s2) { + KdbxHeader h1 = (KdbxHeader) s1.getStreamConfiguration(); + KdbxHeader h2 = (KdbxHeader) s1.getStreamConfiguration(); + return (h1.getVersion() == h2.getVersion() && + h1.getProtectedStreamAlgorithm().equals(h2.getProtectedStreamAlgorithm()) && + h1.getKeyDerivationFunction().equals(h2.getKeyDerivationFunction()) && + h1.getCipherAlgorithm().equals(h2.getCipherAlgorithm())); + } + + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } + +} diff --git a/jaxb/bindings.xml b/jaxb/bindings.xml index fdec4d8b..c6907af8 100644 --- a/jaxb/bindings.xml +++ b/jaxb/bindings.xml @@ -19,7 +19,7 @@ - + + + + + + - + This class, while generated, is not used see {@link org.linguafranca.pwdb.kdbx.jaxb.org.linguafranca.pwdb.base.AbstractJaxbParentedBinding} which is the actual implementation class. + diff --git a/jaxb/pom.xml b/jaxb/pom.xml index 7bfa2b27..d1dfa490 100644 --- a/jaxb/pom.xml +++ b/jaxb/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.2.4 ../pom.xml 4.0.0 @@ -38,13 +38,19 @@ org.linguafranca.pwdb test ${project.version} + test-jar test + - junit - junit - ${junit.version} - test + javax.xml.bind + jaxb-api + 2.3.1 + + + com.sun.xml.bind + jaxb-impl + 2.3.9 @@ -62,9 +68,12 @@ + - generateSources + generateJaxbSources + + @@ -80,9 +89,8 @@ - - ../KDBX.3.1.xsd + ../KDBX.4.xsd bindings.xml diff --git a/jaxb/src/generated/java/META-INF/sun-jaxb.episode b/jaxb/src/generated/java/META-INF/sun-jaxb.episode index 623b3196..c585a686 100644 --- a/jaxb/src/generated/java/META-INF/sun-jaxb.episode +++ b/jaxb/src/generated/java/META-INF/sun-jaxb.episode @@ -5,7 +5,7 @@ This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 See http://java.sun.com/xml/jaxb Any modifications to this file will be lost upon recompilation of the source schema. -Generated on: 2016.10.14 at 01:48:37 PM BST +Generated on: 2023.05.16 at 03:17:45 PM BST --> @@ -18,12 +18,12 @@ Generated on: 2016.10.14 at 01:48:37 PM BST - - - + + + @@ -31,20 +31,20 @@ Generated on: 2016.10.14 at 01:48:37 PM BST - + - - + + + + + - - - diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter1.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter1.java index 5e5da1eb..16005b3b 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter1.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter1.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter2.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter2.java index 2407ecb2..530b9b09 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter2.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter2.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter3.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter3.java index 3510a4dc..aa3295be 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter3.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Adapter3.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/AutoType.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/AutoType.java index 03737571..fe87bdea 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/AutoType.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/AutoType.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // @@ -30,36 +30,36 @@ *

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

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

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

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

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

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

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

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

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

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

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

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

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

- * <complexType name="customData">
- *   <complexContent>
- *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
- *       <sequence>
- *         <any maxOccurs="unbounded" minOccurs="0"/>
- *       </sequence>
- *     </restriction>
- *   </complexContent>
- * </complexType>
+ * <complexType name="customData">
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <sequence>
+ *         <any maxOccurs="unbounded" minOccurs="0"/>
+ *       </sequence>
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
  * 
* * diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomIcons.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomIcons.java index 057ed766..aa4b00d8 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomIcons.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomIcons.java @@ -1,14 +1,15 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // package org.linguafranca.pwdb.kdbx.jaxb.binding; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.UUID; import javax.xml.bind.annotation.XmlAccessType; @@ -25,26 +26,28 @@ *

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

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

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

-     * <complexType>
-     *   <complexContent>
-     *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
-     *       <sequence>
-     *         <element name="UUID" type="{}uuid"/>
-     *         <element name="Data" type="{http://www.w3.org/2001/XMLSchema}base64Binary"/>
-     *       </sequence>
-     *     </restriction>
-     *   </complexContent>
-     * </complexType>
+     * <complexType>
+     *   <complexContent>
+     *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+     *       <sequence>
+     *         <element name="UUID" type="{}uuid"/>
+     *         <element name="Name" type="{http://www.w3.org/2001/XMLSchema}string"/>
+     *         <element name="LastModificationTime" type="{}keepassDateTime"/>
+     *         <element name="Data" type="{http://www.w3.org/2001/XMLSchema}base64Binary"/>
+     *       </sequence>
+     *     </restriction>
+     *   </complexContent>
+     * </complexType>
      * 
* * @@ -111,6 +116,8 @@ public List getIcon() { @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "", propOrder = { "uuid", + "name", + "lastModificationTime", "data" }) public static class Icon { @@ -119,6 +126,12 @@ public static class Icon { @XmlJavaTypeAdapter(Adapter3 .class) @XmlSchemaType(name = "base64Binary") protected UUID uuid; + @XmlElement(name = "Name", required = true) + protected String name; + @XmlElement(name = "LastModificationTime", required = true, type = String.class) + @XmlJavaTypeAdapter(Adapter1 .class) + @XmlSchemaType(name = "dateTime") + protected Date lastModificationTime; @XmlElement(name = "Data", required = true) protected byte[] data; @@ -146,6 +159,54 @@ public void setUUID(UUID value) { this.uuid = value; } + /** + * Gets the value of the name property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getName() { + return name; + } + + /** + * Sets the value of the name property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setName(String value) { + this.name = value; + } + + /** + * Gets the value of the lastModificationTime property. + * + * @return + * possible object is + * {@link String } + * + */ + public Date getLastModificationTime() { + return lastModificationTime; + } + + /** + * Sets the value of the lastModificationTime property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setLastModificationTime(Date value) { + this.lastModificationTime = value; + } + /** * Gets the value of the data property. * diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/History.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/History.java index 2c709c88..d8110ad0 100644 --- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/History.java +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/History.java @@ -1,8 +1,8 @@ // -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.11 +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2016.10.14 at 01:48:37 PM BST +// Generated on: 2023.05.17 at 11:23:19 AM BST // @@ -21,15 +21,15 @@ *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

- * <complexType>
- *   <complexContent>
- *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
- *       <sequence>
- *         <element name="LastModificationTime" type="{}keepassDateTime"/>
- *         <element name="CreationTime" type="{}keepassDateTime"/>
- *         <element name="LastAccessTime" type="{}keepassDateTime"/>
- *         <element name="ExpiryTime" type="{}keepassDateTime"/>
- *         <element name="Expires" type="{}keepassBoolean"/>
- *         <element name="UsageCount" type="{http://www.w3.org/2001/XMLSchema}int"/>
- *         <element name="LocationChanged" type="{}keepassDateTime"/>
- *       </sequence>
- *     </restriction>
- *   </complexContent>
- * </complexType>
+ * <complexType>
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <sequence>
+ *         <element name="LastModificationTime" type="{}keepassDateTime"/>
+ *         <element name="CreationTime" type="{}keepassDateTime"/>
+ *         <element name="LastAccessTime" type="{}keepassDateTime"/>
+ *         <element name="ExpiryTime" type="{}keepassDateTime"/>
+ *         <element name="Expires" type="{}keepassBoolean"/>
+ *         <element name="UsageCount" type="{http://www.w3.org/2001/XMLSchema}int"/>
+ *         <element name="LocationChanged" type="{}keepassDateTime"/>
+ *       </sequence>
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
  * 
* * diff --git a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ValueExtender.java b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ValueExtender.java new file mode 100644 index 00000000..df8407c8 --- /dev/null +++ b/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/ValueExtender.java @@ -0,0 +1,75 @@ +// +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 +// See http://java.sun.com/xml/jaxb +// Any modifications to this file will be lost upon recompilation of the source schema. +// Generated on: 2023.05.17 at 11:23:19 AM BST +// + + +package org.linguafranca.pwdb.kdbx.jaxb.binding; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlSeeAlso; +import javax.xml.bind.annotation.XmlType; +import javax.xml.bind.annotation.XmlValue; + + +/** + * Abstract parent for StringField/Value so that + * to allow creation of a custom mapping that contains extra + * non-serialised (transient) fields. + * + * + *

Java class for ValueExtender complex type. + * + *

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

+ * <complexType name="ValueExtender">
+ *   <simpleContent>
+ *     <extension base="<http://www.w3.org/2001/XMLSchema>string">
+ *     </extension>
+ *   </simpleContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "ValueExtender", propOrder = { + "value" +}) +@XmlSeeAlso({ + org.linguafranca.pwdb.kdbx.jaxb.binding.StringField.Value.class +}) +public abstract class ValueExtender { + + @XmlValue + protected String value; + + /** + * Gets the value of the value property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getValue() { + return value; + } + + /** + * Sets the value of the value property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setValue(String value) { + this.value = value; + } + +} diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java index 34e81eda..1059d52f 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbDatabase.java @@ -17,17 +17,20 @@ package org.linguafranca.pwdb.kdbx.jaxb; 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.stream_3_1.KdbxStreamFormat; -import org.linguafranca.pwdb.kdbx.StreamFormat; +import org.linguafranca.pwdb.kdbx.KdbxHeader; +import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import org.linguafranca.pwdb.kdbx.jaxb.binding.KeePassFile; import org.linguafranca.pwdb.kdbx.jaxb.binding.ObjectFactory; -import org.linguafranca.pwdb.Credentials; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Date; +import java.util.Objects; import java.util.UUID; /** @@ -35,54 +38,53 @@ * * @author jo */ -@SuppressWarnings("WeakerAccess") + public class JaxbDatabase extends AbstractDatabase { - private KeePassFile keePassFile; - private ObjectFactory objectFactory = new ObjectFactory(); - private JaxbGroup root; + private final KeePassFile keePassFile; + private final ObjectFactory objectFactory = new ObjectFactory(); + private final JaxbGroup root; + private StreamFormat streamFormat; public JaxbDatabase() { - this(createEmptyDatabase().getKeePassFile()); + this(createEmptyDatabase().getKeePassFile(), null); } - public JaxbDatabase(KeePassFile keePassFile) { + private JaxbDatabase(KeePassFile keePassFile, StreamFormat streamFormat) { this.keePassFile = keePassFile; this.root = new JaxbGroup(this, keePassFile.getRoot().getGroup()); + this.streamFormat = streamFormat; } public static JaxbDatabase createEmptyDatabase() { - InputStream inputStream = JaxbDatabase.class.getClassLoader().getResourceAsStream("base.kdbx.xml"); KeePassFile keePassFile = new JaxbSerializableDatabase().load(inputStream).keePassFile; keePassFile.getRoot().getGroup().setUUID(UUID.randomUUID()); - return new JaxbDatabase(keePassFile); + return new JaxbDatabase(keePassFile, null); } - public static JaxbDatabase load(Credentials creds, InputStream inputStream) { - StreamFormat format = new KdbxStreamFormat(); - return load(format, creds, inputStream); + public static JaxbDatabase load(Credentials creds, InputStream inputStream) throws IOException { + return load(new KdbxStreamFormat(), creds, inputStream); } @NotNull - public static JaxbDatabase load(StreamFormat format, Credentials creds, InputStream inputStream) { + public static JaxbDatabase load(StreamFormat format, Credentials creds, InputStream inputStream) throws IOException { JaxbSerializableDatabase db = new JaxbSerializableDatabase(); - try { - format.load(db, creds, inputStream); - } catch (IOException e) { - throw new IllegalStateException(e); - } - return new JaxbDatabase(db.getKeePassFile()); + format.load(db, creds, inputStream); + return new JaxbDatabase(db.getKeePassFile(), format); } @Override public void save(Credentials creds, OutputStream outputStream) throws IOException { - save(new KdbxStreamFormat(), creds, outputStream); + if (Objects.isNull(this.streamFormat)){ + this.streamFormat = new KdbxStreamFormat(new KdbxHeader(4)); + } + save(this.streamFormat, creds, outputStream); } - public void save(StreamFormat format, Credentials creds, OutputStream outputStream) throws IOException { - JaxbSerializableDatabase jsd = new JaxbSerializableDatabase(); - jsd.setKeePassFile(this.keePassFile); + public void save(StreamFormat format, Credentials creds, OutputStream outputStream) throws IOException { + keePassFile.getMeta().setGenerator("KeePassJava2-JAXB"); + JaxbSerializableDatabase jsd = new JaxbSerializableDatabase(this.keePassFile); format.save(jsd, creds, outputStream); setDirty(false); } @@ -92,6 +94,32 @@ public JaxbGroup getRootGroup() { return root; } + @Override + public JaxbGroup getRecycleBin() { + UUID recycleBinUuid = this.keePassFile.getMeta().getRecycleBinUUID(); + JaxbGroup g = findGroup(recycleBinUuid); + if (g == null && !isRecycleBinEnabled()) { + return null; + } + if (g == null) { + g = newGroup("Recycle Bin"); + getRootGroup().addGroup(g); + this.keePassFile.getMeta().setRecycleBinUUID(g.getUuid()); + this.keePassFile.getMeta().setRecycleBinChanged(new Date()); + } + return g; + } + + @Override + public boolean isRecycleBinEnabled() { + return this.keePassFile.getMeta().getRecycleBinEnabled(); + } + + @Override + public void enableRecycleBin(boolean enable) { + this.keePassFile.getMeta().setRecycleBinEnabled(enable); + } + @Override public JaxbGroup newGroup() { return new JaxbGroup(this); @@ -158,4 +186,14 @@ public KeePassFile getKeePassFile() { ObjectFactory getObjectFactory() { return objectFactory; } + + public void createBinary(byte[] value, Integer index) { + JaxbSerializableDatabase.addBinary(getKeePassFile(), getObjectFactory(), index, value); + } + + @SuppressWarnings("unchecked") + @Override + public StreamFormat getStreamFormat(){ + return streamFormat; + } } diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java index 17e61ccd..1eafe10d 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntry.java @@ -16,6 +16,7 @@ package org.linguafranca.pwdb.kdbx.jaxb; +import org.jetbrains.annotations.NotNull; import org.linguafranca.pwdb.base.AbstractEntry; import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.jaxb.binding.*; @@ -51,7 +52,7 @@ public JaxbEntry(JaxbDatabase jaxbDatabase) { delegate.getString().add(field); } - Date now = new Date(); + Date now = new Date(System.currentTimeMillis() / 1000L * 1000L); // to nearest lower second Times times = new Times(); times.setLastModificationTime(now); times.setCreationTime(now); @@ -103,6 +104,26 @@ public void setProperty(String name, String value) { touch(); } + @Override + public boolean removeProperty(String name) throws IllegalArgumentException { + if (STANDARD_PROPERTY_NAMES.contains(name)) throw new IllegalArgumentException("may not remove property: " + name); + + StringField toRemove = null; + for (StringField field: delegate.getString()){ + if (field.getKey().equals(name)) { + toRemove = field; + break; + } + } + if (toRemove == null) { + return false; + } else { + delegate.getString().remove(toRemove); + touch(); + return true; + } + } + @Override public List getPropertyNames() { List result = new ArrayList<>(); @@ -154,12 +175,7 @@ public void setBinaryProperty(String name, byte[] value) { } max++; - // create a new binary to put in the store - Binaries.Binary newBin = database.getObjectFactory().createBinariesBinary(); - newBin.setID(max); - newBin.setValue(Helpers.zipBinaryContent(value)); - newBin.setCompressed(true); - binaryList.add(newBin); + database.createBinary(value, max); // make a reference to it from the entry BinaryField binaryField = database.getObjectFactory().createBinaryField(); @@ -171,6 +187,25 @@ public void setBinaryProperty(String name, byte[] value) { touch(); } + @Override + public boolean removeBinaryProperty(String name) throws UnsupportedOperationException { + BinaryField toRemove = null; + for (BinaryField binaryField : delegate.getBinary()) { + if (binaryField.getKey().equals(name)) { + toRemove = binaryField; + break; + } + } + + if (toRemove == null) { + return false; + } else { + delegate.getBinary().remove(toRemove); + touch(); + return true; + } + } + @Override public List getBinaryPropertyNames() { List result = new ArrayList<>(); @@ -189,7 +224,7 @@ public JaxbGroup getParent() { } @Override - public UUID getUuid() { + public @NotNull UUID getUuid() { return delegate.getUUID(); } @@ -219,11 +254,22 @@ public boolean getExpires() { return delegate.getTimes().getExpires(); } + @Override + public void setExpires(boolean expires) { + delegate.getTimes().setExpires(true); + } + @Override public Date getExpiryTime() { return delegate.getTimes().getExpiryTime(); } + @Override + public void setExpiryTime(Date expiryTime) throws IllegalArgumentException { + if (expiryTime == null) throw new IllegalArgumentException("expiryTime may not be null"); + delegate.getTimes().setExpiryTime(expiryTime); + } + @Override public Date getLastModificationTime() { return delegate.getTimes().getLastModificationTime(); diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbGroup.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbGroup.java index 461f9087..1ceddb9e 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbGroup.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbGroup.java @@ -16,6 +16,7 @@ package org.linguafranca.pwdb.kdbx.jaxb; +import org.jetbrains.annotations.NotNull; import org.linguafranca.pwdb.base.AbstractGroup; import org.linguafranca.pwdb.kdbx.jaxb.binding.JaxbEntryBinding; import org.linguafranca.pwdb.kdbx.jaxb.binding.JaxbGroupBinding; @@ -63,6 +64,11 @@ public boolean isRootGroup() { return database.getRootGroup().equals(this); } + @Override + public boolean isRecycleBin() { + return database.getKeePassFile().getMeta().getRecycleBinUUID().equals(this.getUuid()); + } + @Override public JaxbGroup getParent() { if (delegate.parent == null) { @@ -196,6 +202,7 @@ public void setIcon(JaxbIcon icon) { this.delegate.setIconID(icon.getIndex()); } + @NotNull @Override public JaxbDatabase getDatabase() { return database; diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java index e80dc944..188af88b 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabase.java @@ -16,24 +16,28 @@ package org.linguafranca.pwdb.kdbx.jaxb; +import com.sun.xml.txw2.output.IndentingXMLStreamWriter; import org.apache.commons.codec.binary.Base64; -import org.linguafranca.pwdb.kdbx.SerializableDatabase; -import org.linguafranca.pwdb.kdbx.StreamEncryptor; -import org.linguafranca.pwdb.kdbx.jaxb.binding.JaxbEntryBinding; -import org.linguafranca.pwdb.kdbx.jaxb.binding.JaxbGroupBinding; -import org.linguafranca.pwdb.kdbx.jaxb.binding.KeePassFile; -import org.linguafranca.pwdb.kdbx.jaxb.binding.StringField; +import org.jetbrains.annotations.NotNull; +import org.linguafranca.pwdb.SerializableDatabase; +import org.linguafranca.pwdb.kdbx.Helpers; +import org.linguafranca.pwdb.kdbx.jaxb.base.ValueBinding; +import org.linguafranca.pwdb.kdbx.jaxb.binding.*; +import org.linguafranca.pwdb.security.StreamEncryptor; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; -import java.io.IOException; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; import java.io.InputStream; import java.io.OutputStream; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * @author jo @@ -41,36 +45,53 @@ @SuppressWarnings("WeakerAccess") public class JaxbSerializableDatabase implements SerializableDatabase { + private final ObjectFactory objectFactory = new ObjectFactory(); protected KeePassFile keePassFile; private StreamEncryptor encryption; + public JaxbSerializableDatabase() { + + } + + public JaxbSerializableDatabase(KeePassFile keePassFile) { + this.keePassFile = keePassFile; + } + + public static void addBinary(KeePassFile keePassFile, ObjectFactory objectFactory, int index, byte[] value) { + // create a new binary to put in the store + Binaries.Binary newBin = objectFactory.createBinariesBinary(); + newBin.setID(index); + newBin.setValue(Helpers.zipBinaryContent(value)); + newBin.setCompressed(true); + if (keePassFile.getMeta().getBinaries() == null) { + keePassFile.getMeta().setBinaries(objectFactory.createBinaries()); + } + keePassFile.getMeta().getBinaries().getBinary().add(newBin); + } @Override public JaxbSerializableDatabase load(InputStream inputStream) { try { - JAXBContext jc = JAXBContext.newInstance(KeePassFile.class); + JAXBContext jc = JAXBContext.newInstance(KeePassFile.class, ValueBinding.class); Unmarshaller u = jc.createUnmarshaller(); u.setListener(new Unmarshaller.Listener() { @Override public void afterUnmarshal(Object target, Object parent) { - try { - if (target instanceof StringField.Value) { - StringField.Value value = (StringField.Value) target; - if (value.getProtected() !=null && value.getProtected()) { - byte[] encrypted = Base64.decodeBase64(value.getValue().getBytes()); - String decrypted = new String(encryption.decrypt(encrypted), "UTF-8"); - value.setValue(decrypted); - value.setProtected(false); - } + if (target instanceof StringField.Value) { + StringField.Value value = (StringField.Value) target; + if (value.getProtected() != null && value.getProtected()) { + byte[] encrypted = Base64.decodeBase64(value.getValue().getBytes()); + String decrypted = new String(encryption.decrypt(encrypted), StandardCharsets.UTF_8); + value.setValue(decrypted); + value.setProtected(null); + value.protectOnOutput=true; } - if (target instanceof JaxbGroupBinding && (parent instanceof JaxbGroupBinding)) { - ((JaxbGroupBinding) target).parent = ((JaxbGroupBinding) parent); - } - if (target instanceof JaxbEntryBinding && (parent instanceof JaxbGroupBinding)) { - ((JaxbEntryBinding) target).parent = ((JaxbGroupBinding) parent); - } - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException(); + } + if (target instanceof JaxbGroupBinding && (parent instanceof JaxbGroupBinding)) { + ((JaxbGroupBinding) target).parent = ((JaxbGroupBinding) parent); + } + if (target instanceof JaxbEntryBinding && (parent instanceof JaxbGroupBinding)) { + ((JaxbEntryBinding) target).parent = ((JaxbGroupBinding) parent); } } }); @@ -82,7 +103,93 @@ public void afterUnmarshal(Object target, Object parent) { } @Override - public void save(OutputStream outputStream) throws IOException { + public void save(OutputStream outputStream) { + + try { + JAXBContext jc = JAXBContext.newInstance(KeePassFile.class); + Marshaller marshaller = jc.createMarshaller(); + marshaller.setListener(createMarshallerListener(getToEncrypt())); + + XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newInstance(); + XMLStreamWriter xmlStreamWriter = xmlOutputFactory.createXMLStreamWriter(outputStream); + + // this tidies up output of boolean ="False" attributes and indentation, which is incorrect + // in marshaller and also uses tabs rather than spaces, which is more economical + IndentingXMLStreamWriter writer = new IndentingXMLStreamWriter(xmlStreamWriter){ + + @Override + public void writeStartDocument() throws XMLStreamException { + setIndentStep("\t"); + super.writeStartDocument(); + } + + @Override + public void writeAttribute(String localName, String value) throws XMLStreamException { + if (localName.equals("ProtectInMemory")) { + return; + } + if (localName.equals("Protected")) { + if (!value.equalsIgnoreCase("true")){ + return; + } + } + super.writeAttribute(localName, value); + } + }; + marshaller.marshal(keePassFile, writer); + + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + /** + * Creates a listener for marshalling that will rewrite fields that need encryption + */ + private Marshaller.Listener createMarshallerListener(List toEncrypt) { + return new Marshaller.Listener() { + String savedValue=""; + + // this changes the content on save, so we need to change it back again, see comment below + @Override + public void beforeMarshal(Object source) { + if (source instanceof StringField) { + StringField field = (StringField) source; + if (toEncrypt.contains(field.getKey()) || field.getValue().protectOnOutput) { + savedValue = field.getValue().getValue(); + byte [] encrypted = encryption.encrypt(field.getValue().getValue().getBytes(StandardCharsets.UTF_8)); + byte [] base64Encoded = Base64.encodeBase64(encrypted); + field.getValue().setValue(new String(base64Encoded)); + field.getValue().setProtected(true); + } else { + field.getValue().setProtected(false); + } + field.getValue().setProtectInMemory(false); + } + } + + // turns out that undoing the content change we made in beforeMarshal is the easiest way of doing this + // after a couple of days of looking at it. Making a clone before serialization + // is not practical and creating an adapter is not practical either, believe me, I tried. + // That said, if you are a JAXB whizz, and you know better ... + @Override + public void afterMarshal(Object source) { + if (source instanceof StringField) { + StringField field = (StringField) source; + if (field.getValue().getProtected()) { + field.getValue().setValue(savedValue); + field.getValue().setProtected(false); + } + } + } + }; + } + + /** + * Create a list of names of properties that should be encrypted by default + */ + @NotNull + private List getToEncrypt() { final List toEncrypt = new ArrayList<>(); if (keePassFile.getMeta().getMemoryProtection().getProtectTitle()) { toEncrypt.add(org.linguafranca.pwdb.Entry.STANDARD_PROPERTY_NAME_TITLE); @@ -99,34 +206,10 @@ public void save(OutputStream outputStream) throws IOException { if (keePassFile.getMeta().getMemoryProtection().getProtectNotes()) { toEncrypt.add(org.linguafranca.pwdb.Entry.STANDARD_PROPERTY_NAME_NOTES); } - try { - JAXBContext jc = JAXBContext.newInstance(KeePassFile.class); - Marshaller u = jc.createMarshaller(); - u.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); - u.setListener(new Marshaller.Listener() { - @Override - public void beforeMarshal(Object source) { - try { - if (source instanceof StringField) { - StringField field = (StringField) source; - if (toEncrypt.contains(field.getKey())) { - byte[] encrypted = encryption.encrypt(field.getValue().getValue().getBytes()); - String b64 = new String(Base64.encodeBase64(encrypted), "UTF-8"); - field.getValue().setValue(b64); - field.getValue().setProtected(true); - } - } - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException(); - } - } - }); - u.marshal(keePassFile, outputStream); - } catch (JAXBException e) { - throw new IllegalStateException(e); - } + return toEncrypt; } + @Override public StreamEncryptor getEncryption() { return encryption; @@ -147,6 +230,23 @@ public void setHeaderHash(byte[] hash) { keePassFile.getMeta().setHeaderHash(hash); } + @Override + public void addBinary(int index, byte[] value) { + addBinary(keePassFile, objectFactory, index, value); + } + + @Override + public byte[] getBinary(int index) { + return keePassFile.getMeta().getBinaries().getBinary().get(index).getValue(); + } + + @Override + public int getBinaryCount() { + if (Objects.isNull(keePassFile.getMeta().getBinaries())) { + return 0; + } + return keePassFile.getMeta().getBinaries().getBinary().size(); + } public KeePassFile getKeePassFile() { return keePassFile; diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/base/ValueBinding.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/base/ValueBinding.java new file mode 100644 index 00000000..a89fc2a2 --- /dev/null +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/base/ValueBinding.java @@ -0,0 +1,29 @@ +package org.linguafranca.pwdb.kdbx.jaxb.base; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlTransient; +import javax.xml.bind.annotation.XmlValue; + +/** + * Ancestor class of StringField.Value, providing a way of flagging protection + * separate from @Protected and @ProtectInMemory + */ +@XmlAccessorType(XmlAccessType.FIELD) +public abstract class ValueBinding { + + @XmlTransient + public boolean protectOnOutput; + + @XmlValue + public String value; + + public String getValue(){ + return value; + } + + public void setValue(String string){ + value = string; + } + +} diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/util/DateAdapter.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/util/DateAdapter.java index 33c6bbd6..da5f78b3 100644 --- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/util/DateAdapter.java +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/util/DateAdapter.java @@ -30,6 +30,7 @@ public static String toString(Date date) { } public static Date fromString(String string) { + // this is found in the base template if (string.equals("${creationDate}")) { return new Date(); } diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/util/JaxbUtil.java b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/util/JaxbUtil.java new file mode 100644 index 00000000..7eb700f1 --- /dev/null +++ b/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/util/JaxbUtil.java @@ -0,0 +1,28 @@ +package org.linguafranca.pwdb.kdbx.jaxb.util; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.util.JAXBSource; +import javax.xml.namespace.QName; + +public class JaxbUtil { + + // https://stackoverflow.com/questions/879453/how-to-make-a-deep-copy-of-jaxb-object-like-xmlbean-xmlobject-copy + // however it does not copy transients which we need it to, so we don't use it + public static T deepCopyJAXB(T object, Class clazz) { + try { + JAXBContext jaxbContext = JAXBContext.newInstance(clazz); + JAXBElement contentObject = new JAXBElement(new QName(clazz.getSimpleName()), clazz, object); + JAXBSource source = new JAXBSource(jaxbContext, contentObject); + return jaxbContext.createUnmarshaller().unmarshal(source, clazz).getValue(); + } catch (JAXBException e) { + throw new RuntimeException(e); + } + } + + public static T deepCopyJAXB(T object) { + if(object==null) throw new RuntimeException("Can't guess at class"); + return deepCopyJAXB(object, (Class) object.getClass()); + } +} diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntryTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV3Test.java similarity index 68% rename from jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntryTest.java rename to jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV3Test.java index 46763a43..d014cc9b 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbEntryTest.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV3Test.java @@ -17,7 +17,7 @@ package org.linguafranca.pwdb.kdbx.jaxb; import org.linguafranca.pwdb.Database; -import org.linguafranca.pwdb.checks.EntryChecks; +import org.linguafranca.pwdb.checks.BinaryPropertyChecks; import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.Credentials; @@ -28,9 +28,9 @@ /** * @author jo */ -public class JaxbEntryTest extends EntryChecks { +public class JaxbBinaryPropertyV3Test extends BinaryPropertyChecks { - public JaxbEntryTest() { + public JaxbBinaryPropertyV3Test() throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment.kdbx"); database = JaxbDatabase.load(new KdbxCreds("123".getBytes()),inputStream); } @@ -39,4 +39,20 @@ public JaxbEntryTest() { public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { database.save(credentials,outputStream); } + + + @Override + public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + return JaxbDatabase.load(credentials, inputStream); + } + + @Override + public Database newDatabase() { + return new JaxbDatabase(); + } + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } + } diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.java new file mode 100644 index 00000000..9a9fcd26 --- /dev/null +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBinaryPropertyV4Test.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.jaxb; + +import org.junit.Ignore; +import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.StreamFormat; +import org.linguafranca.pwdb.checks.BinaryPropertyChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.KdbxHeader; +import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * @author jo + */ +@Ignore +public class JaxbBinaryPropertyV4Test extends BinaryPropertyChecks { + + public JaxbBinaryPropertyV4Test() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha20-Argon2-Attachment.kdbx"); + database = JaxbDatabase.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 JaxbDatabase.load(credentials, inputStream); + } + + @Override + public Database newDatabase() { + return new JaxbDatabase(); + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } +} \ No newline at end of file diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbPropertyValueTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbPropertyValueTest.java new file mode 100644 index 00000000..06f0d611 --- /dev/null +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbPropertyValueTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024 Jo Rabin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.linguafranca.pwdb.kdbx.jaxb; + +import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.checks.PropertyValueChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class JaxbPropertyValueTest extends PropertyValueChecks { + + public JaxbPropertyValueTest() throws IOException { + super(false); + } + + + @Override + public Database createDatabase() throws IOException { + return new JaxbDatabase(); + } + + @Override + public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { + database.save(credentials, outputStream); + } + + @Override + public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + try { + return JaxbDatabase.load(credentials, inputStream); + } catch (Exception e) { + throw new IOException(e); + } + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } +} diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbRecycleBinTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbRecycleBinTest.java new file mode 100644 index 00000000..c837e3c6 --- /dev/null +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbRecycleBinTest.java @@ -0,0 +1,12 @@ +package org.linguafranca.pwdb.kdbx.jaxb; + +import org.linguafranca.pwdb.checks.RecycleBinChecks; + +/** + * @author jo + */ +public class JaxbRecycleBinTest extends RecycleBinChecks { + public JaxbRecycleBinTest() { + database = new JaxbDatabase(); + } +} diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSaveAndReloadTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSaveAndReloadTest.java index 7c5d2a9b..37eb4341 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSaveAndReloadTest.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSaveAndReloadTest.java @@ -16,11 +16,11 @@ package org.linguafranca.pwdb.kdbx.jaxb; -import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.checks.SaveAndReloadChecks; import org.linguafranca.pwdb.kdbx.KdbxCreds; -import org.linguafranca.pwdb.kdbx.StreamFormat; -import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.kdbx.KdbxHeader; import java.io.IOException; import java.io.InputStream; @@ -29,24 +29,24 @@ /** * @author jo */ -public class JaxbSaveAndReloadTest extends SaveAndReloadChecks { +public class JaxbSaveAndReloadTest extends SaveAndReloadChecks { @Override - public Database getDatabase() { + public JaxbDatabase getDatabase() { return new JaxbDatabase(); } @Override - public Database getDatabase(String name, Credentials credentials) { + public JaxbDatabase getDatabase(String name, Credentials credentials) throws IOException { InputStream inputStream = getClass().getClassLoader().getResourceAsStream(name); return JaxbDatabase.load(credentials, inputStream); } @Override - public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { + public void saveDatabase(JaxbDatabase database, Credentials credentials, OutputStream outputStream) throws IOException { database.save(credentials, outputStream); } @Override - public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + public JaxbDatabase loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { return JaxbDatabase.load(credentials, inputStream); } @@ -54,4 +54,15 @@ public Database loadDatabase(Credentials credentials, InputStream inputStream) t public Credentials getCreds(byte[] creds) { return new KdbxCreds(creds); } + + + @Override + public boolean verifyStreamFormat(StreamFormat s1, StreamFormat s2) { + KdbxHeader h1 = (KdbxHeader) s1.getStreamConfiguration(); + KdbxHeader h2 = (KdbxHeader) s1.getStreamConfiguration(); + return (h1.getVersion() == h2.getVersion() && + h1.getProtectedStreamAlgorithm().equals(h2.getProtectedStreamAlgorithm()) && + h1.getKeyDerivationFunction().equals(h2.getKeyDerivationFunction()) && + h1.getCipherAlgorithm().equals(h2.getCipherAlgorithm())); + } } diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabaseTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabaseTest.java index 1509ed1b..420630a8 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabaseTest.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbSerializableDatabaseTest.java @@ -17,25 +17,33 @@ package org.linguafranca.pwdb.kdbx.jaxb; import org.junit.Test; +import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.Visitor; import org.linguafranca.pwdb.kdbx.KdbxCreds; import java.io.InputStream; +import java.io.PrintStream; + +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo */ public class JaxbSerializableDatabaseTest { + + static PrintStream printStream = getTestPrintStream(); + @Test public void createEmptyDatabase() throws Exception { JaxbDatabase db = JaxbDatabase.createEmptyDatabase(); - db.save(new KdbxCreds.None(), System.out); + db.save(new StreamFormat.None(), new KdbxCreds.None(), printStream); } @Test public void loadXml() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test123.kdbx"); JaxbDatabase database = JaxbDatabase.load(new KdbxCreds("123".getBytes()), inputStream); - database.visit(new Visitor.Print()); + database.visit(new Visitor.Print(printStream)); + database.save(new StreamFormat.None(), new KdbxCreds.None(), printStream); } } \ No newline at end of file diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbTest.java index 3320ce8b..f1e47dfa 100644 --- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbTest.java +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbTest.java @@ -21,11 +21,15 @@ import org.linguafranca.pwdb.kdbx.jaxb.binding.StringField; import javax.xml.bind.*; +import java.io.PrintStream; + +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo */ public class JaxbTest { + static PrintStream printStream = getTestPrintStream(); @Test public void unmarshal() throws JAXBException { @@ -34,7 +38,7 @@ public void unmarshal() throws JAXBException { u.setEventHandler(new ValidationEventHandler() { @Override public boolean handleEvent(ValidationEvent event) { - System.out.println(event.getLocator().getLineNumber() +": "+ event.getMessage()); + printStream.println(event.getLocator().getLineNumber() +": "+ event.getMessage()); return true; } }); @@ -43,13 +47,13 @@ public boolean handleEvent(ValidationEvent event) { public void afterUnmarshal(Object target, Object parent) { if (target instanceof StringField.Value) { StringField.Value value = (StringField.Value) target; - System.out.println(value.getValue()); + printStream.println(value.getValue()); } super.afterUnmarshal(target, parent); } }); KeePassFile kpf = (KeePassFile) u.unmarshal(getClass().getClassLoader().getResourceAsStream("ExampleDatabase.xml")); - System.out.println(kpf.getMeta().getDatabaseDescription()); + printStream.println(kpf.getMeta().getDatabaseDescription()); } } diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbV4LoadTest.java b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbV4LoadTest.java new file mode 100644 index 00000000..be1f8695 --- /dev/null +++ b/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbV4LoadTest.java @@ -0,0 +1,34 @@ +package org.linguafranca.pwdb.kdbx.jaxb; + +import org.junit.Test; +import org.linguafranca.pwdb.Entry; +import org.linguafranca.pwdb.Visitor; +import org.linguafranca.pwdb.kdbx.KdbxCreds; + +import java.io.InputStream; +import java.io.PrintStream; + +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; + +/** + * @author jo + */ +public class JaxbV4LoadTest { + + static PrintStream printStream = getTestPrintStream(); + + @Test + public void loadKdbxV4() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); + JaxbDatabase database = JaxbDatabase.load(new KdbxCreds("123".getBytes()), inputStream); + database.visit(new Visitor.Print(printStream)); + // test what happens to dates in V4 + database.visit(new Visitor.Default(){ + @Override + public void visit(Entry entry) { + printStream.println(entry.getCreationTime()); + } + }); + } + +} diff --git a/kdb/pom.xml b/kdb/pom.xml index 2ae3af4e..18c1dc95 100644 --- a/kdb/pom.xml +++ b/kdb/pom.xml @@ -3,14 +3,14 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.2.4 ../pom.xml 4.0.0 KeePassJava2-kdb KeePassJava2 :: KDB - Contains an implementaion that can load KDB files. + Contains an implementation that can load KDB files. @@ -22,14 +22,8 @@ org.linguafranca.pwdb test ${project.version} - test - - - junit - junit - ${junit.version} + test-jar test - \ No newline at end of file diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java index 616c331e..b578d062 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbCredentials.java @@ -16,9 +16,13 @@ package org.linguafranca.pwdb.kdb; +import com.google.common.io.ByteStreams; import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.security.Encryption; +import org.bouncycastle.util.encoders.Hex; +import java.io.IOException; +import java.io.InputStream; import java.security.MessageDigest; /** @@ -31,11 +35,12 @@ public interface KdbCredentials extends Credentials { /** * Password only credentials */ - public static class Password implements KdbCredentials { - private byte [] key; + class Password implements KdbCredentials { + + private final byte [] key; public Password(byte[] password) { - MessageDigest md = Encryption.getMessageDigestInstance(); + MessageDigest md = Encryption.getSha256MessageDigestInstance(); this.key = md.digest(password); } @@ -44,4 +49,36 @@ public byte[] getKey() { return key; } } + + /** + * Key file credentials + */ + class KeyFile implements KdbCredentials { + + private final byte[] key; + + public KeyFile(byte[] password, InputStream inputStream) { + MessageDigest md = Encryption.getSha256MessageDigestInstance(); + byte[] pwKey = md.digest(password); + md.update(pwKey); + + try { + byte [] keyFileData = ByteStreams.toByteArray(inputStream); + if (keyFileData.length == 64) { + keyFileData = Hex.decode(keyFileData); + key = md.digest(keyFileData); + return; + } + throw new IllegalStateException("Invalid length key file " + keyFileData.length); + } catch (IOException e) { + throw new IllegalStateException("Could not read key file", e); + } + } + + @Override + public byte[] getKey() { + return this.key; + } + + } } diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbDatabase.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbDatabase.java index f60088a9..fbb8634e 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbDatabase.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbDatabase.java @@ -16,12 +16,8 @@ package org.linguafranca.pwdb.kdb; -import org.linguafranca.pwdb.Group; -import org.linguafranca.pwdb.Icon; -import org.linguafranca.pwdb.Visitor; +import org.linguafranca.pwdb.*; import org.linguafranca.pwdb.base.AbstractDatabase; -import org.linguafranca.pwdb.Credentials; -import org.linguafranca.pwdb.Entry; import java.io.IOException; import java.io.InputStream; @@ -36,7 +32,7 @@ */ public class KdbDatabase extends AbstractDatabase { private String description; - private KdbGroup rootGroup; + private final KdbGroup rootGroup; static SimpleDateFormat isoDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); @@ -103,10 +99,16 @@ public void setDescription(String description) { } @Override - public void save(Credentials credentials, OutputStream outputStream) throws IOException { + public void save(Credentials credentials, OutputStream outputStream) { throw new UnsupportedOperationException("Cannot save KDB files in this implementation"); } + @Override + public void save(StreamFormat streamFormat, Credentials credentials, + OutputStream outputStream) { + throw new UnsupportedOperationException(); + } + @Override public boolean shouldProtect(String propertyName) { return false; @@ -140,4 +142,41 @@ public void startVisit(Group group) { } } } + + @Override + public boolean isRecycleBinEnabled() { + return false; + } + + @Override + public void enableRecycleBin(boolean enable) { + if (enable) { + throw new UnsupportedOperationException("KDB files don't have a recycle bin"); + } + } + + @Override + public KdbGroup getRecycleBin() { + return null; + } + + @Override + public boolean supportsNonStandardPropertyNames() { + return false; + } + + @Override + public boolean supportsBinaryProperties() { + return false; + } + + @Override + public boolean supportsRecycleBin() { + return false; + } + + @Override + public StreamFormat getStreamFormat(){ + return null; + } } diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java index cd832f83..ac9927b7 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbEntry.java @@ -16,6 +16,7 @@ package org.linguafranca.pwdb.kdb; +import org.jetbrains.annotations.NotNull; import org.linguafranca.pwdb.Entry; import org.linguafranca.pwdb.Icon; @@ -40,9 +41,10 @@ public class KdbEntry extends AbstractEntry getPropertyNames() { return new ArrayList<>(Entry.STANDARD_PROPERTY_NAMES); @@ -82,7 +94,7 @@ public KdbGroup getParent() { } @Override - public UUID getUuid() { + public @NotNull UUID getUuid() { return uuid; } @@ -174,7 +186,8 @@ public Date getLastAccessTime() { return lastAccessTime; } - void setExpiryTime(Date expiryTime) { + public void setExpiryTime(Date expiryTime) { + if (expiryTime == null) throw new IllegalArgumentException("expiryTime may not be null"); this.expiryTime = expiryTime; } @@ -223,9 +236,14 @@ public List getBinaryPropertyNames() { throw new UnsupportedOperationException(); } + @Override + public void setExpires(boolean expires) { + this.expires = expires; + } + @Override public boolean getExpires() { - return false; + return expires; } @Override diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbGroup.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbGroup.java index 24373464..3df785cb 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbGroup.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbGroup.java @@ -16,6 +16,7 @@ package org.linguafranca.pwdb.kdb; +import org.jetbrains.annotations.NotNull; import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.Group; import org.linguafranca.pwdb.Icon; @@ -209,7 +210,13 @@ public String toString() { return getPath() + String.format(" (%s) %s [%d]", uuid.toString(), time, flags); } + @NotNull public KdbDatabase getDatabase() { return database; } + + @Override + public boolean isRecycleBin() { + return false; + } } diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbHeader.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbHeader.java index 728605b3..976bf318 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbHeader.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbHeader.java @@ -16,11 +16,13 @@ package org.linguafranca.pwdb.kdb; -import org.linguafranca.pwdb.security.Encryption; +import org.linguafranca.pwdb.security.Aes; import javax.crypto.Cipher; -import java.io.IOException; import java.io.InputStream; +import java.security.MessageDigest; + +import static org.linguafranca.pwdb.security.Encryption.getSha256MessageDigestInstance; /** * This class stores the encryption details of a KDB file and provides a method to create @@ -53,16 +55,18 @@ public class KdbHeader { * @param key key * @param inputStream an encrypted stream * @return a decrypted stream - * @throws IOException */ - public InputStream createDecryptedInputStream(byte[] key, InputStream inputStream) throws IOException { + public InputStream createDecryptedInputStream(byte[] key, InputStream inputStream) { Cipher cipher; if ((flags & FLAG_RIJNDAEL) == 0) { throw new IllegalStateException("StreamEncryptor algorithm is not supported"); } - byte[] finalKeyDigest = Encryption.getFinalKeyDigest(key, masterSeed, transformSeed, transformRounds); - return Encryption.getDecryptedInputStream(inputStream, finalKeyDigest, encryptionIv); + byte[] transformedKeyDigest = Aes.getTransformedKey(key, transformSeed, transformRounds); + MessageDigest md = getSha256MessageDigestInstance(); + md.update(masterSeed); + byte[] finalKeyDigest = md.digest(transformedKeyDigest); + return Aes.getInstance().getDecryptedInputStream(inputStream, finalKeyDigest, encryptionIv); } public int getFlags() { diff --git a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java index 07c88214..51e357de 100644 --- a/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java +++ b/kdb/src/main/java/org/linguafranca/pwdb/kdb/KdbSerializer.java @@ -17,7 +17,6 @@ package org.linguafranca.pwdb.kdb; import com.google.common.io.LittleEndianDataInputStream; -import org.linguafranca.pwdb.base.AbstractGroup; import org.linguafranca.pwdb.Credentials; import org.linguafranca.pwdb.Group; import org.linguafranca.pwdb.security.Encryption; @@ -28,23 +27,20 @@ import java.nio.ByteBuffer; import java.security.DigestInputStream; import java.security.MessageDigest; -import java.util.Arrays; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.UUID; +import java.util.*; /** * This class provides support for reading a KDB stream and constructing an in memory database. - *

+ *

* A KDB stream consists of: *

    *
  1. 16 bits of file signature
  2. *
  3. An unencrypted header containing encryption and other information such as the count of groups and entries
  4. - *
  5. In an Encrypted form:
  6. + *
  7. In an Encrypted form: *
      *
    1. Repeated serialised form of all Groups
    2. *
    3. Repeated serialised form of all Entries
    4. - *
    + *
* * * @author jo @@ -58,7 +54,7 @@ private KdbSerializer() { } /** - * Construct a KDB database from the supplied inputstream. + * Construct a KDB database from the supplied inputStream. * * @param credentials the credentials * @param kdbHeader a header to be populated with values read from the stream @@ -79,7 +75,7 @@ public static KdbDatabase createKdbDatabase(Credentials credentials, KdbHeader k InputStream decryptedInputStream = kdbHeader.createDecryptedInputStream(credentials.getKey(), inputStream); // Wrap the decrypted stream in a digest stream - MessageDigest digest = Encryption.getMessageDigestInstance(); + MessageDigest digest = Encryption.getSha256MessageDigestInstance(); DigestInputStream digestInputStream = new DigestInputStream(decryptedInputStream, digest); // Start the dataInput at wherever we have got to in the stream @@ -212,7 +208,7 @@ private static KdbGroup deserializeGroup(KdbGroup lastGroup, DataInput dataInput group.setFlags(readInt(dataInput)); break; default: - throw new IllegalStateException("Unknown field type"); + throw new IllegalStateException("Unknown field type " + String.valueOf(fieldType)); } } dataInput.readInt(); @@ -295,13 +291,13 @@ private static void deserializeEntry(KdbDatabase database, DataInput dataInput) /** * Figure out who the parent of this group is. - *

+ *

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

+ *

* Since the database tree is built progressively the group passed in the "lastGroup" parameter has - * already been knitted into the hierarchy and so either this group is a sub group of that group or is a subgroup + * already been knitted into the hierarchy and so either this group is a subgroup of that group or is a subgroup * of the nearest ancestor to the last group that has a level less than the group we are reading. * * @param lastGroup the last group we saw in the stream @@ -312,7 +308,7 @@ private static void deserializeEntry(KdbDatabase database, DataInput dataInput) private static KdbGroup computeParentGroup(KdbGroup lastGroup, int level) { // the level of the last group int lastLevel = lastGroup.computedLevel(); - // if we are one greater then we are its child + // if we are one greater, then we are its child if (level == lastLevel + 1) { return lastGroup; } @@ -330,7 +326,7 @@ private static KdbGroup computeParentGroup(KdbGroup lastGroup, int level) { } /** - * Stucture is in this format: 00YYYYYY YYYYYYMM MMDDDDDH HHHHMMMM MMSSSSSS + * Structure is in this format: 00YYYYYY YYYYYYMM MMDDDDDH HHHHMMMM MMSSSSSS * * @param buffer 5 bytes containing a packed date * @return a date constructed from the buffer @@ -363,7 +359,14 @@ public static Date unpackDate(byte[] buffer) { int year = (int) longValue & 0xFFF; // just to work around the deprecation on the similar Date constructor - return new GregorianCalendar(year, month - 1, day, hour, minute, second).getTime(); + GregorianCalendar cal = new GregorianCalendar(); + // I think the time is stored in local time but anyway, let's say it's UTC for the sake of argument + cal.setTimeZone(TimeZone.getTimeZone("UTC")); + //noinspection MagicConstant + cal.set(year, month - 1, day, hour, minute, second); + // otherwise we seem to end up with arbitrary millis + cal.set(GregorianCalendar.MILLISECOND, 0); + return cal.getTime(); } /**** diff --git a/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbDatabaseLoaderTest.java b/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbDatabaseLoaderTest.java index 5b5ecc15..d375e45f 100644 --- a/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbDatabaseLoaderTest.java +++ b/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbDatabaseLoaderTest.java @@ -34,5 +34,6 @@ public KdbDatabaseLoaderTest() throws IOException { Credentials credentials = new KdbCredentials.Password("123".getBytes()); // open database. super.database = KdbDatabase.load(credentials, inputStream); + super.skipDateCheck = true; } } diff --git a/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbDatabaseTest.java b/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbDatabaseTest.java index cbcb24a0..84d08972 100644 --- a/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbDatabaseTest.java +++ b/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbDatabaseTest.java @@ -16,11 +16,14 @@ package org.linguafranca.pwdb.kdb; +import org.junit.Test; import org.linguafranca.pwdb.checks.BasicDatabaseChecks; import org.linguafranca.pwdb.Database; import java.io.IOException; +import static org.junit.Assert.assertFalse; + /** * @author Jo */ @@ -33,4 +36,11 @@ public Database createDatabase() throws IOException { public KdbDatabaseTest() throws IOException { super(); } + + @Test + public void supportedFunctionalityTest(){ + assertFalse(database.supportsBinaryProperties()); + assertFalse(database.supportsNonStandardPropertyNames()); + assertFalse(database.supportsRecycleBin()); + } } diff --git a/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbSerializerTest.java b/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbSerializerTest.java index 25921a2a..72e426dd 100644 --- a/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbSerializerTest.java +++ b/kdb/src/test/java/org/linguafranca/pwdb/kdb/KdbSerializerTest.java @@ -21,16 +21,22 @@ import org.linguafranca.pwdb.Visitor; import java.io.InputStream; +import java.io.PrintStream; + +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo */ public class KdbSerializerTest { + static PrintStream printStream = getTestPrintStream(); + + @Test public void testCreateKdbDatabase() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test123.kdb"); Database database = KdbDatabase.load(new KdbCredentials.Password("123".getBytes()), inputStream); - database.visit(new Visitor.Print()); + database.visit(new Visitor.Print(printStream)); } } \ No newline at end of file diff --git a/kdb/src/test/java/org/linguafranca/pwdb/kdb/KeyFileTest.java b/kdb/src/test/java/org/linguafranca/pwdb/kdb/KeyFileTest.java new file mode 100644 index 00000000..c78ba10a --- /dev/null +++ b/kdb/src/test/java/org/linguafranca/pwdb/kdb/KeyFileTest.java @@ -0,0 +1,24 @@ +package org.linguafranca.pwdb.kdb; + +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.Assert.assertEquals; + +/** + * @author jo + */ +public class KeyFileTest { + + @Test + public void openKdbWithKeyFile() throws IOException { + InputStream key = getClass().getClassLoader().getResourceAsStream("kdb.key"); + KdbCredentials creds = new KdbCredentials.KeyFile("123".getBytes(), key); + InputStream is = getClass().getClassLoader().getResourceAsStream("kdbwithkey.kdb"); + KdbDatabase db = KdbDatabase.load(creds, is); + assertEquals(1, db.getRootGroup().getGroupsCount()); + assertEquals("General", db.getRootGroup().getGroups().get(0).getName()); + } +} \ No newline at end of file diff --git a/kdbx/pom.xml b/kdbx/pom.xml index 07762de2..c67cff19 100644 --- a/kdbx/pom.xml +++ b/kdbx/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.2.4 ../pom.xml 4.0.0 @@ -41,31 +41,14 @@ commons-codec commons-codec - ${commons-codec.version} - - - org.jetbrains - annotations - ${annotations.version} - - - com.intellij - annotations - ${annotations.intellij.version} - provided - + 1.17.0 + org.linguafranca.pwdb test ${project.version} - test - - - junit - junit - ${junit.version} + test-jar test - \ No newline at end of file diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingInputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingInputStream.java new file mode 100644 index 00000000..e301e625 --- /dev/null +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingInputStream.java @@ -0,0 +1,90 @@ +package org.linguafranca.pwdb.hashedblock; + +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Class allows the forwarding (as a filter) and collection of read bytes as a buffer + * - e.g. to provide for HMAC operations + * + * @author jo + */ +public class CollectingInputStream extends FilterInputStream { + + private ByteArrayOutputStream collectedBytes = new ByteArrayOutputStream(); + private boolean collecting = true; + + /** + * Create a collecting stream which is set to collect from the get go + * + * @param in the input stream to forward/collect + */ + public CollectingInputStream(InputStream in) { + this(in, true); + } + + /** + * Create a collecting stream + * + * @param in the input stream to forward/collect + * @param collecting whether the initial state is collecting or not + */ + public CollectingInputStream(InputStream in, boolean collecting) { + super(in); + this.collecting = collecting; + } + + @Override + public int read() throws IOException { + int result = super.read(); + if (collecting && result != -1) { + collectedBytes.write(result); + } + return result; + } + + @Override + public int read(@NotNull byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(@NotNull byte[] b, int off, int len) throws IOException { + int result = super.read(b, off, len); + if (collecting && result != -1) { + collectedBytes.write(b, off, result); + } + return result; + } + + @Override + public long skip(long n) throws IOException { + return super.skip(n); + } + + /** + * Get the collected bytes as a byte array + */ + public byte[] getCollectedBytes() { + return collectedBytes.toByteArray(); + } + + /** + * True if we are currently collecting bytes + */ + public boolean isCollecting() { + return collecting; + } + + /** + * Cganeg the state of collecting bytes + * @param collecting true to collect + */ + public void setCollecting(boolean collecting) { + this.collecting = collecting; + } +} diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingOutputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingOutputStream.java new file mode 100644 index 00000000..90919b20 --- /dev/null +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/CollectingOutputStream.java @@ -0,0 +1,77 @@ +package org.linguafranca.pwdb.hashedblock; + +import org.jetbrains.annotations.NotNull; + +import java.io.*; + +/** + * Class allows the forwarding (as a filter) and collection of written bytes as a buffer + * - e.g. to provide for HMAC operations + * + * @author jo + */ +public class CollectingOutputStream extends FilterOutputStream { + + private final ByteArrayOutputStream collectedBytes = new ByteArrayOutputStream(); + private boolean collecting = true; + + /** + * Create a collecting stream which is set to collect from the get go + * + * @param out the output stream to forward/collect + */ + public CollectingOutputStream(OutputStream out) { + this(out, true); + } + + /** + * Create a collecting stream + * + * @param out the output stream to forward/collect + * @param collecting whether the initial state is collecting or not + */ + public CollectingOutputStream(OutputStream out, boolean collecting) { + super(out); + this.collecting = collecting; + } + + @Override + public void write(int toWrite) throws IOException { + super.write(toWrite); + if (collecting) { + collectedBytes.write(toWrite); + } + } + + @Override + public void write(byte @NotNull [] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte @NotNull [] b, int off, int len) throws IOException { + super.write(b, off, len); + } + + /** + * Get the collected bytes as a byte array + */ + public byte[] getCollectedBytes() { + return collectedBytes.toByteArray(); + } + + /** + * True if we are currently collecting bytes + */ + public boolean isCollecting() { + return collecting; + } + + /** + * Cganeg the state of collecting bytes + * @param collecting true to collect + */ + public void setCollecting(boolean collecting) { + this.collecting = collecting; + } +} diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockInputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockInputStream.java index 80dabb74..47d96986 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockInputStream.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockInputStream.java @@ -49,17 +49,10 @@ */ public class HashedBlockInputStream extends InputStream { - private static MessageDigest md5; - static { - try { - md5 = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } - } private static final int HASH_SIZE = 32; private static final byte[] ZERO_HASH = new byte[HASH_SIZE]; + private final MessageDigest sha256; private long expectedSequenceNumber = 0; private boolean littleEndian = false; private boolean done = false; @@ -68,6 +61,7 @@ public class HashedBlockInputStream extends InputStream { /** * Create a Big Endian Hash Block Input Stream + * * @param inputStream the input stream containing the hash blocks */ public HashedBlockInputStream(InputStream inputStream) { @@ -76,12 +70,18 @@ public HashedBlockInputStream(InputStream inputStream) { /** * Create a Hash Block Input Stream with choice of endian encoding - * @param inputStream the input stream containing the hash blocks + * + * @param inputStream the input stream containing the hash blocks * @param littleEndian true if the stream is little endian encoded */ public HashedBlockInputStream(InputStream inputStream, boolean littleEndian) { this.inputStream = inputStream; this.littleEndian = littleEndian; + try { + sha256 = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } } @Override @@ -105,7 +105,8 @@ public void close() throws IOException { /** * Gets bytes from the internal buffer and replenishes the buffer as necessary - * @param b a byte array to fill + * + * @param b a byte array to fill * @param offset the offset to strat from * @param length the number of bytes to return * @return the number of bytes actually returned, , -1 if end of file @@ -131,6 +132,7 @@ protected int get(byte[] b, int offset, int length) throws IOException { /** * Reload the internal buffer from the underlying input stream + * * @throws IOException */ protected void load() throws IOException { @@ -165,8 +167,8 @@ protected void load() throws IOException { readFully(readBuffer); // check the hash - md5.update(readBuffer); - if (!Arrays.equals(md5.digest(), hash)) { + sha256.update(readBuffer); + if (!Arrays.equals(sha256.digest(), hash)) { throw new IllegalStateException("MD5 check failed while reading HashBlock"); } blockInputStream = new ByteArrayInputStream(readBuffer); @@ -174,6 +176,7 @@ protected void load() throws IOException { /** * Read an unsigned 4 byte int decoding from the endian format + * * @return a long holding the value read * @throws IOException */ @@ -188,6 +191,7 @@ private long readUInt() throws IOException { /** * Fill the buffer passed + * * @param buffer the buffer to fill * @throws IOException if the buffer could not be filled */ diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockOutputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockOutputStream.java index 0af0e45e..e376e556 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockOutputStream.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HashedBlockOutputStream.java @@ -47,18 +47,11 @@ */ public class HashedBlockOutputStream extends OutputStream { - private static MessageDigest md5; - static { - try { - md5 = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } - } private static final int BLOCK_SIZE = 8 * 1024; private static final int HASH_SIZE = 32; private static final byte[] ZERO_HASH = new byte[HASH_SIZE]; + private final MessageDigest sha256; private int nextSequenceNumber = 0; private boolean littleEndian = false; private OutputStream outputStream; @@ -67,6 +60,7 @@ public class HashedBlockOutputStream extends OutputStream { /** * Create a BigEndian Hash Block Output Stream + * * @param outputStream the output stream to receive the hash blocks */ public HashedBlockOutputStream(OutputStream outputStream) { @@ -75,12 +69,18 @@ public HashedBlockOutputStream(OutputStream outputStream) { /** * Create a Hash Block Output Stream with choice of endian encoding + * * @param outputStream the output stream to receive the hash blocks * @param littleEndian true to encode in a little endian way */ public HashedBlockOutputStream(OutputStream outputStream, boolean littleEndian) { this.outputStream = outputStream; this.littleEndian = littleEndian; + try { + sha256 = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } } @Override @@ -118,7 +118,8 @@ public void close() throws IOException { /** * Writes to the internal buffer, and writes to the underlying output stream * as necessary as {@link #BLOCK_SIZE} blocks - * @param b the byte array to write + * + * @param b the byte array to write * @param offset offset in the byte array * @param length number of bytes to write * @throws IOException @@ -139,7 +140,8 @@ protected void put(byte[] b, int offset, int length) throws IOException { } /** - * Saqve the internal buffer to the underlying stream as a hash block + * Save the internal buffer to the underlying stream as a hash block + * * @throws IOException */ protected void save() throws IOException { @@ -152,8 +154,8 @@ protected void save() throws IOException { // calculate the hash of the buffer byte[] buffer = blockOutputStream.toByteArray(); - md5.update(buffer); - outputStream.write(md5.digest()); + sha256.update(buffer); + outputStream.write(sha256.digest()); // write the buffer's length writeInt(buffer.length); @@ -170,6 +172,7 @@ protected void save() throws IOException { /** * Write a 4 byte int value to the underlying stream in appropriate endian format + * * @param value the value to write * @throws IOException */ @@ -178,6 +181,7 @@ protected void writeInt(int value) throws IOException { if (littleEndian) { output = Integer.reverseBytes(value); } - outputStream.write(new byte[]{(byte) (output >> 24), (byte) (output >> 16), (byte) (output >> 8), (byte) output}); + outputStream.write(new byte[]{(byte) (output >> 24), (byte) (output >> 16), + (byte) (output >> 8), (byte) output}); } } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java new file mode 100644 index 00000000..9a7825df --- /dev/null +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockInputStream.java @@ -0,0 +1,159 @@ +package org.linguafranca.pwdb.hashedblock; + +import com.google.common.io.LittleEndianDataInputStream; +import org.jetbrains.annotations.NotNull; + +import javax.crypto.Mac; +import java.io.*; +import java.nio.ByteOrder; +import java.util.Arrays; + +import static org.linguafranca.pwdb.kdbx.Helpers.toBytes; +import static org.linguafranca.pwdb.security.Encryption.getHMacSha256Instance; +import static org.linguafranca.pwdb.security.Encryption.transformHmacKey; + +/** + * Takes an underlying stream formatted as HMAC Hashed Blocks and provides + * the content of the blocks as a stream. + *

+ * An HMac block consists of + *

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

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

+ * KeePass streams are Little Endian. + */ +public class HmacBlockInputStream extends FilterInputStream { + + private final ByteOrder byteOrder; + private final byte[] key; + private volatile ByteArrayInputStream bufferStream; + private final DataInput input; + private boolean finished; + private int blockCount = 0; + + /** + * Create a (big endian) HMac Block input stream + * + * @param key the key digest + * @param inputStream the stream to process + * @throws IOException when something horrid happens + */ + public HmacBlockInputStream(byte[] key, InputStream inputStream) throws IOException { + this(key, inputStream, false); + } + + /** + * Create an HMac Block input stream + * + * @param key the key digest + * @param inputStream the stream to process + * @param littleEndian true if the stream is little endian + * @throws IOException when something horrid happens + */ + public HmacBlockInputStream(byte[] key, InputStream inputStream, boolean littleEndian) throws IOException { + super(inputStream); + this.byteOrder = littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN; + this.key = key; + if (littleEndian) { + input = new LittleEndianDataInputStream(in); + } else { + input = new DataInputStream(in); + } + getBlock(); + } + + private void getBlock() throws IOException { + // get the HMAC + byte[] hmacSha256 = new byte[32]; + input.readFully(hmacSha256); + + // get the block size + int blockSize = input.readInt(); + if (blockSize == 0) { + finished = true; + } + + // read the new block + byte[] buffer = new byte[blockSize]; + input.readFully(buffer); + + verifyHmac(buffer, blockCount, hmacSha256); + + // create a new internal stream for the block + bufferStream = new ByteArrayInputStream(buffer); + blockCount++; + } + + /** + * HmacBlockStream.cs ReadSafeBlock + * + * @param buffer the buffer to check + * @param blockNumber the block number of this buffer + * @param hmacSha256 the hmac to verify + */ + private void verifyHmac(byte[] buffer, long blockNumber, byte[] hmacSha256) { + final byte[] transformedKey = transformHmacKey(this.key, toBytes(blockNumber, byteOrder)); + final Mac mac = getHMacSha256Instance(transformedKey); + mac.update(toBytes(blockNumber, byteOrder)); + mac.update(toBytes(buffer.length, byteOrder)); + if (!Arrays.equals(mac.doFinal(buffer), hmacSha256)) { + throw new IllegalStateException("Block HMAC does not match"); + } + +/* // using bouncy castle + HMac hmac = new HMac(new SHA256Digest()); + hmac.init(new KeyParameter(getHmacBlockKey(this.key, blockNumber, byteOrder))); + hmac.update(Helpers.toBytes(blockNumber, byteOrder), 0, 8); + hmac.update(Helpers.toBytes(buffer.length, byteOrder), 0, 4); + hmac.update(buffer, 0, buffer.length); + byte[] computedHmacSha256 = new byte[32]; + hmac.doFinal(computedHmacSha256, 0); + if (!Arrays.equals(computedHmacSha256, hmacSha256)) { + throw new IllegalStateException("Block HMAC does not match"); + } +*/ + } + + @Override + public int read(@NotNull byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(@NotNull byte[] b, int off, int len) throws IOException { + if (finished) { + return -1; + } + int bytesRead = bufferStream.read(b, off, len); + if (bufferStream.available() == 0) { + getBlock(); + } + return bytesRead; + } + + @Override + public int read() throws IOException { + if (finished) { + return -1; + } + int result = bufferStream.read(); + if (result == -1) { + getBlock(); + if (finished) { + return -1; + } + result = bufferStream.read(); + if (result == -1) { + throw new IOException("Can't replenish buffer"); + } + } + return result; + } +} diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockOutputStream.java b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockOutputStream.java new file mode 100644 index 00000000..5dd0d252 --- /dev/null +++ b/kdbx/src/main/java/org/linguafranca/pwdb/hashedblock/HmacBlockOutputStream.java @@ -0,0 +1,152 @@ +package org.linguafranca.pwdb.hashedblock; + +import static org.linguafranca.pwdb.kdbx.Helpers.toBytes; +import static org.linguafranca.pwdb.security.Encryption.getHMacSha256Instance; +import static org.linguafranca.pwdb.security.Encryption.transformHmacKey; + +import java.io.*; +import java.nio.ByteOrder; +import javax.crypto.Mac; +import org.jetbrains.annotations.NotNull; +import com.google.common.io.LittleEndianDataOutputStream; + +/** + * Takes a stream of (encrypted GZIPped) data and formats it as HMAC Hashed Blocks to the + * underlying output stream. + *

+ * An HMac block consists of + *

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

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

+ * Streams in Keepass are Little Endian. + */ + +public class HmacBlockOutputStream extends FilterOutputStream { + private static final int BLOCK_SIZE = (int) Math.pow(2, 20); // blocks can technically be bigger but KeePass supposedly splits them into this size + + private final byte[] key; + + private final ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(); + /** the encrypted, compressed output stream */ + private final DataOutput output; + private final ByteOrder byteOrder; + private long nextSequenceNumber = 0; + private boolean isClosed = false; + + /** + * Create a BigEndian HmacBlockOutputStream + * @param key credentials for this database + * @param outputStream the output stream to receive the hash blocks + */ + public HmacBlockOutputStream(byte [] key, OutputStream outputStream) throws IOException { + this(key, outputStream, false); + } + + /** + * Create a HmacBlockOutputStream with choice of endian encoding + * @param key credentials for this database + * @param outputStream the output stream to receive the hash blocks + * @param littleEndian true to encode in a little endian way + */ + public HmacBlockOutputStream(byte [] key, OutputStream outputStream, boolean littleEndian) throws IOException { + super(outputStream); + this.key = key; + this.byteOrder = littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN; + if (this.byteOrder.equals(ByteOrder.LITTLE_ENDIAN)) { + //noinspection UnstableApiUsage + this.output = new LittleEndianDataOutputStream(out); + } else { + this.output = new DataOutputStream(out); + } + } + + @Override + public void close() throws IOException { + if (isClosed) { + throw new EOFException(); + } + // clear unwritten data + flush(); + // write final block + writeBlock(new byte [0]); + // push the contents to disk etc. + out.flush(); + out.close(); + isClosed = true; + } + + private void writeBlock(byte [] buffer) throws IOException { + final byte[] transformedKey = transformHmacKey(this.key, toBytes(nextSequenceNumber, byteOrder)); + final Mac mac = getHMacSha256Instance(transformedKey); + mac.update(toBytes(nextSequenceNumber++, byteOrder)); + mac.update(toBytes(buffer.length, byteOrder)); + mac.update(buffer); + byte[] finalMac = mac.doFinal(); + output.write(finalMac); + + // write the buffer's length + output.writeInt(buffer.length); + output.write(buffer); + out.flush(); + } + + /** + * Writes to the internal buffer, and writes to the underlying output stream + * as necessary as {@link #BLOCK_SIZE} blocks + * @param b the byte array to write + * @param offset offset in the byte array + * @param length number of bytes to write + */ + protected void put(byte[] b, int offset, int length) throws IOException { + if (isClosed) { + throw new EOFException(); + } + + while (length > 0) { + int bytesToWrite = Math.min(BLOCK_SIZE - outputBuffer.size(), length); + outputBuffer.write(b, offset, bytesToWrite); + + if (outputBuffer.size() >= BLOCK_SIZE) { + save(); + } + + offset += bytesToWrite; + length -= bytesToWrite; + } + } + + /** + * Save the internal buffer to the underlying stream as a Hmac block + */ + protected void save() throws IOException { + if (outputBuffer.size() == 0) { + return; + } + writeBlock(outputBuffer.toByteArray()); + outputBuffer.reset(); + } + + @Override + public void write(int i) throws IOException { + byte[] buf = new byte[1]; + buf[0] = (byte) i; + put(buf, 0, 1); + } + + @Override + public void write(byte @NotNull [] b, int offset, int count) throws IOException { + put(b, offset, count); + } + + @Override + public void flush() throws IOException { + save(); + } +} \ No newline at end of file diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java index e6c6854a..d789bd15 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/Helpers.java @@ -19,23 +19,32 @@ import com.google.common.io.ByteStreams; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Hex; -import org.jetbrains.annotations.Nullable; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; + import java.nio.ByteBuffer; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.nio.ByteOrder; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Date; import java.util.UUID; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; + /** - * @author jo + * The class provides helpers to marshal and unmarshal values of KDBX files */ public class Helpers { + /** + * Oftentimes we have no way of communicating which version we are using, say in an adapter that + * is buried deep in the internals of JAXB marshalling + */ + public static ThreadLocal isV4 = ThreadLocal.withInitial(() -> false); + public static String base64FromUuid(UUID uuid) { byte[] buffer = new byte[16]; ByteBuffer b = ByteBuffer.wrap(buffer); @@ -67,30 +76,74 @@ public static UUID uuidFromBase64(String base64) { return new UUID(b.getLong(), b.getLong(8)); } + /* --- Boolean --- + Booleans are deliberately tri-valued true, false and null + */ public static Boolean toBoolean(String value) { - return value.toLowerCase().equals("true") || value.toLowerCase().equals("Yes") || value.equals("1"); + if (value.equalsIgnoreCase("null")) { + return null; + } + return value.equalsIgnoreCase("true") || value.equalsIgnoreCase("yes") || value.equals("1"); } public static String fromBoolean(Boolean value) { - return value == null ? "False" : value ? "True" : "False"; + return value == null ? "null" : (value ? "True" : "False"); } - private static SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + /* --- Dates + V3 dates are serialised as ISO8601 using Zulu (Z) TZD - but will deserialize more leniently + V4 dates are base64 encoded seconds since midnight 0001-01-01 + --- */ + + // we use this for formatting a Date which doesn't have a time zone, and we are assuming + // that date is in fact GMT + //public static final SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + public static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssz"); + public static final Date baseDate = Date.from(ZonedDateTime.parse("0001-01-01T00:00:00Z", dateTimeFormatter).toInstant()); + // in V3 this is just a date, in V4 it's a base64 encoded serial number of seconds after the base date above public static Date toDate(String value) { try { - return inFormat.parse(value); - } catch (ParseException e) { - throw new IllegalStateException(e); + ZonedDateTime zdt = ZonedDateTime.parse(value); + Instant instant = zdt.toInstant(); + return Date.from(instant); + } catch (DateTimeParseException e) { + // let's see if it is a V4 date } + // V4 dates are base 64 encoded seconds since baseDate + byte [] b = decodeBase64Content(value.getBytes()); + long secondsSinceBaseDate = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getLong(); + Instant instant = Instant.ofEpochSecond(secondsSinceBaseDate + baseDate.getTime()/1000); + return Date.from(instant); } + /** + * Formats the value according to the value of {@link Helpers#isV4} + * @param value a date + * @return a formatted date + */ public static String fromDate(Date value) { - return inFormat.format(value); + return isV4.get() ? fromDateV4(value) : fromDateV3(value); + } + public static String fromDateV3(Date value) { + return dateTimeFormatter.format(value.toInstant().atZone(ZoneId.of("Z"))); + } + public static String fromDateV4(Date value) { + long keepassInstant = value.toInstant().toEpochMilli() - baseDate.getTime(); + long secondsSinceBaseDate = keepassInstant / 1000; + byte [] asBytes = toBytes(secondsSinceBaseDate, ByteOrder.LITTLE_ENDIAN); + return encodeBase64Content(asBytes); } - @Nullable - public static byte[] decodeBase64Content(byte[] content, boolean isCompressed) { + + + /* --- Base64 --- */ + + public static byte[] decodeBase64Content(byte[] content) { + return decodeBase64Content(content, false); + } + + public static byte[] decodeBase64Content (byte[] content, boolean isCompressed) { byte[] value = Base64.decodeBase64(content); if (isCompressed) { return unzipBinaryContent(value); @@ -98,7 +151,6 @@ public static byte[] decodeBase64Content(byte[] content, boolean isCompressed) { return value; } - @Nullable public static byte[] unzipBinaryContent(byte[] content) { ByteArrayInputStream bais = new ByteArrayInputStream(content); try { @@ -109,6 +161,10 @@ public static byte[] unzipBinaryContent(byte[] content) { } } + public static String encodeBase64Content(byte[] value) { + return encodeBase64Content(value, false); + } + public static String encodeBase64Content(byte[] value, boolean isCompressed) { if (!isCompressed) { return Base64.encodeBase64String(value); @@ -129,4 +185,20 @@ public static byte[] zipBinaryContent(byte[] value) { } return baos.toByteArray(); } + + public static byte[] toBytes(long value, ByteOrder byteOrder) { + byte[] longBuffer = new byte [8]; + ByteBuffer.wrap(longBuffer) + .order(byteOrder) + .putLong(value); + return longBuffer; + } + + public static byte[] toBytes(int value, ByteOrder byteOrder) { + byte[] longBuffer = new byte [4]; + ByteBuffer.wrap(longBuffer) + .order(byteOrder) + .putInt(value); + return longBuffer; + } } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxCredentials.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxCredentials.java index 0163fc0f..3863dbee 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxCredentials.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxCredentials.java @@ -46,7 +46,7 @@ class KeyFile implements KdbxCredentials { * @param inputStream inputstream of the keyfile */ public KeyFile(@NotNull byte[] password, @NotNull InputStream inputStream) { - MessageDigest md = Encryption.getMessageDigestInstance(); + MessageDigest md = Encryption.getSha256MessageDigestInstance(); byte[] pwKey = md.digest(password); md.update(pwKey); @@ -62,7 +62,7 @@ public KeyFile(@NotNull byte[] password, @NotNull InputStream inputStream) { * @param inputStream inputstream of the keyfile */ public KeyFile(@NotNull InputStream inputStream) { - MessageDigest md = Encryption.getMessageDigestInstance(); + MessageDigest md = Encryption.getSha256MessageDigestInstance(); byte[] keyFileData = KdbxKeyFile.load(inputStream); if (keyFileData == null) { @@ -85,7 +85,7 @@ class Password implements KdbxCredentials { private final byte[] key; public Password(@NotNull byte[] password) { - MessageDigest md = Encryption.getMessageDigestInstance(); + MessageDigest md = Encryption.getSha256MessageDigestInstance(); byte[] digest = md.digest(password); key = md.digest(digest); } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxCreds.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxCreds.java index edf3e17f..dff3d242 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxCreds.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxCreds.java @@ -38,7 +38,7 @@ public class KdbxCreds implements Credentials { * @param inputStream inputstream of the keyfile */ public KdbxCreds(@NotNull byte[] password, @NotNull InputStream inputStream) { - MessageDigest md = Encryption.getMessageDigestInstance(); + MessageDigest md = Encryption.getSha256MessageDigestInstance(); byte[] pwKey = md.digest(password); md.update(pwKey); @@ -54,7 +54,7 @@ public KdbxCreds(@NotNull byte[] password, @NotNull InputStream inputStream) { * @param inputStream inputstream of the keyfile */ public KdbxCreds(@NotNull InputStream inputStream) { - MessageDigest md = Encryption.getMessageDigestInstance(); + MessageDigest md = Encryption.getSha256MessageDigestInstance(); byte[] keyFileData = KdbxKeyFile.load(inputStream); if (keyFileData == null) { throw new IllegalStateException("Could not read key file"); @@ -64,7 +64,7 @@ public KdbxCreds(@NotNull InputStream inputStream) { public KdbxCreds(@NotNull byte[] password) { - MessageDigest md = Encryption.getMessageDigestInstance(); + MessageDigest md = Encryption.getSha256MessageDigestInstance(); byte[] digest = md.digest(password); key = md.digest(digest); } diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java new file mode 100644 index 00000000..ecfdb585 --- /dev/null +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxHeader.java @@ -0,0 +1,463 @@ +/* + * 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.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.StreamConfiguration; +import org.linguafranca.pwdb.security.*; + +import javax.crypto.Mac; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static org.linguafranca.pwdb.security.Encryption.getSha256MessageDigestInstance; + +/** + * This class represents the header portion of a KeePass KDBX file or stream. The header is received in + * plain text and describes the encryption and compression of the remainder of the file. + *

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

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

+ * While KDBX streams are Little-Endian, data is passed to and from this class in standard Java byte order. + */ +@SuppressWarnings("WeakerAccess") +public class KdbxHeader implements StreamConfiguration { + /** + * The ordinal 0 represents uncompressed and 1 GZip compressed + */ + @SuppressWarnings("WeakerAccess") + public enum CompressionFlags { + NONE, GZIP + } + + private final List allowableVersions = new ArrayList<>(Arrays.asList(3, 4)); + + + /* version of the file - most significant 2 bytes i.e. 0x0302 is version 3 */ + private int version; + + protected UUID cipherUuid; + private byte[] masterSeed; + private byte[] encryptionIv; + + /* whether the data is compressed */ + private CompressionFlags compressionFlags; + + /* V3 fields */ + private byte[] transformSeed; + private long transformRounds; + + /* header (V3) inner header (v4) */ + private byte[] innerRandomStreamKey; + private Encryption.ProtectedStreamAlgorithm protectedStreamAlgorithm; + private CipherAlgorithm cipherAlgorithm; + private KeyDerivationFunction keyDerivationFunction; + + /* these bytes appear in cipher text immediately following the header (V3) */ + private byte[] streamStartBytes; + + /* dictionaries in V4 */ + private VariantDictionary kdfParameters; + // TODO implement V4 custom data + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private VariantDictionary customData; + + /* + * binaries in V4 + * first byte, if set to 1 indicates "protected" remainder is the payload + */ + List binaries = new ArrayList<>(); + + /* V3 not transmitted as part of the header, used in the XML payload, so calculated + * on transmission or receipt */ + private byte[] headerHash; + + /* the bytes that compose the outer header, required for V4 to calculate the HMac */ + private byte[] headerBytes; + + // Make static and try to use SHA1PRNG per + // https://stackoverflow.com/questions/137212/how-to-deal-with-a-slow-securerandom-generator + // and comment on issue #12. If you don't like this then you can of course set this + // to something else + public static SecureRandom random; + static { + try { + random = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + random = new SecureRandom(); + } + } + /** + * Provides for choice of version number and crypto options for V3 and V4 + */ + interface KdbxHeaderOptions { + int getVersion(); + CipherAlgorithm getCipherAlgorithm(); + KeyDerivationFunction getKeyDerivationFunction(); + Encryption.ProtectedStreamAlgorithm getProtectedStreamAlgorithm(); + } + + /** + * Default values for crypto options + */ + enum KdbxHeaderOpts implements KdbxHeaderOptions{ + V3_AES_SALSA_20(3, Encryption.Cipher.AES, Encryption.KeyDerivationFunction.AES, Encryption.ProtectedStreamAlgorithm.SALSA_20), + V4_AES_ARGON_CHA_CHA (4, Encryption.Cipher.AES, Encryption.KeyDerivationFunction.ARGON2, Encryption.ProtectedStreamAlgorithm.CHA_CHA_20); + + // + final int version; + final CipherAlgorithm algorithm; + final KeyDerivationFunction kdf; + final Encryption.ProtectedStreamAlgorithm protectedStreamAlgorithm; + + + KdbxHeaderOpts(int version, Encryption.Cipher cipher, Encryption.KeyDerivationFunction kdf, Encryption.ProtectedStreamAlgorithm protectedStreamAlgorithm) { + this.version = version; + this.algorithm = cipher; + this.kdf = kdf; + this.protectedStreamAlgorithm = protectedStreamAlgorithm; + } + + @Override + public int getVersion() { + return version; + } + + @Override + public CipherAlgorithm getCipherAlgorithm() { + return algorithm; + } + + @Override + public KeyDerivationFunction getKeyDerivationFunction() { + return kdf; + } + + @Override + public Encryption.ProtectedStreamAlgorithm getProtectedStreamAlgorithm() { + return protectedStreamAlgorithm; + } + // + } + + /** + * Construct a default version 3 KDBX header + */ + public KdbxHeader(){ + this(KdbxHeaderOpts.V3_AES_SALSA_20); + } + + /** + * Construct a default KDBX header with AES/AES/SALSA_20 + */ + public KdbxHeader(int version) { + this(version ==3 ? KdbxHeaderOpts.V3_AES_SALSA_20 : KdbxHeaderOpts.V4_AES_ARGON_CHA_CHA); + } + + + public KdbxHeader(KdbxHeaderOptions opts) { + this.version = opts.getVersion(); + setCipherAlgorithm(opts.getCipherAlgorithm()); + setKeyDerivationFunction(opts.getKeyDerivationFunction()); + setProtectedStreamAlgorithm(opts.getProtectedStreamAlgorithm()); + cipherUuid = opts.getCipherAlgorithm().getCipherUuid(); + + compressionFlags = CompressionFlags.GZIP; + masterSeed = random.generateSeed(32); + transformSeed = random.generateSeed(32); + transformRounds = 6000; + encryptionIv = random.generateSeed(16); + innerRandomStreamKey = random.generateSeed(32); + streamStartBytes = new byte[32]; + } + + /** + * Compute the Hmac Key Digest + * from "KdbxFile.cs Computekeys" + * + * @param credentials the credentials + * @return the digest + */ + public byte[] getHmacKey(Credentials credentials) { + MessageDigest md = Encryption.getSha512MessageDigestInstance(); + md.update(getMasterSeed()); + md.update(getTransformedKeyDigest(credentials.getKey())); + return md.digest(new byte[]{1}); + } + + /** + * Verify the header Hmac + * + * @param key the transformed Hmac Key for the header + * @param bytes the bytes to compare to verify + */ + public void verifyHeaderHmac(byte[] key, byte[] bytes) { + Mac mac = Encryption.getHMacSha256Instance(key); + byte[] computedHmacSha256 = mac.doFinal(getHeaderBytes()); + if (!Arrays.equals(computedHmacSha256, bytes)) { + throw new IllegalStateException("Header HMAC does not match"); + } + } + + /** + * Create a decrypted input stream using supplied digest and this header + * apply decryption to the passed encrypted input stream + * + * @param digest the key digest + * @param inputStream the encrypted input stream + * @return a decrypted stream + */ + public InputStream createDecryptedStream(byte[] digest, InputStream inputStream) { + // return digest of master seed and hash + MessageDigest md = getSha256MessageDigestInstance(); + md.update(masterSeed); + byte[] finalKeyDigest = md.digest(getTransformedKeyDigest(digest)); + CipherAlgorithm ca = Encryption.Cipher.getCipherAlgorithm(cipherUuid); + return ca.getDecryptedInputStream(inputStream, finalKeyDigest, encryptionIv); + } + + public StreamEncryptor getInnerStreamEncryptor() { + return Encryption.ProtectedStreamAlgorithm.getStreamEncryptor(getProtectedStreamAlgorithm(), getInnerRandomStreamKey()); + } + + /** + * Takes the composite credentials and transforms them according to the underlying KDF algorithm. + * @param digest the credentials digested + * @return the transformed digest + */ + public byte[] getTransformedKeyDigest(byte[] digest) { + // v3 doesn't have a kdf therefore AES + if (kdfParameters == null) { + return Aes.getTransformedKey(digest, transformSeed, transformRounds); + } + KeyDerivationFunction kdf = Encryption.KeyDerivationFunction.getKdf(kdfParameters.mustGet("$UUID").asUuid()); + return kdf.getTransformedKey(digest, kdfParameters); + } + + /** + * Create an unencrypted outputStream using the supplied digest and this header + * and use the supplied output stream to write encrypted data. + * + * @param digest the key digest + * @param outputStream the output stream which is the destination for encrypted data + * @return an output stream to write unencrypted data to + */ + public OutputStream createEncryptedStream(byte[] digest, OutputStream outputStream) { + // return digest of master seed and hash + MessageDigest md = getSha256MessageDigestInstance(); + md.update(masterSeed); + byte[] finalKeyDigest = md.digest(getTransformedKeyDigest(digest)); + return cipherAlgorithm.getEncryptedOutputStream(outputStream, finalKeyDigest, getEncryptionIv()); + } + + // + public byte[] getTransformSeed() { + if (version < 4) { + return transformSeed; + } + return kdfParameters.mustGet(Aes.KdfKeys.ParamSeed).asByteArray(); + } + + public long getTransformRounds() { + if (version < 4) { + return transformRounds; + } + return kdfParameters.mustGet(Aes.KdfKeys.ParamRounds).asLong(); + } + + public UUID getCipherUuid() { + return cipherUuid; + } + + public CompressionFlags getCompressionFlags() { + return compressionFlags; + } + + public byte[] getMasterSeed() { + return masterSeed; + } + + public byte[] getEncryptionIv() { + return encryptionIv; + } + + public byte[] getInnerRandomStreamKey() { + return innerRandomStreamKey; + } + + public byte[] getStreamStartBytes() { + return streamStartBytes; + } + + public CipherAlgorithm getCipherAlgorithm() { + return cipherAlgorithm; + } + + public KeyDerivationFunction getKeyDerivationFunction() { + return keyDerivationFunction; + } + + public Encryption.ProtectedStreamAlgorithm getProtectedStreamAlgorithm() { + return protectedStreamAlgorithm; + } + + public byte[] getHeaderHash() { + return headerHash; + } + + public int getVersion() { + return version; + } + + // V4 + public VariantDictionary getKdfParameters() { + return kdfParameters; + } + + public StreamEncryptor getStreamEncryptor() { + return Encryption.ProtectedStreamAlgorithm.getStreamEncryptor(getProtectedStreamAlgorithm(), this.innerRandomStreamKey); + } + + public void setCompressionFlags(int flags) { + this.compressionFlags = CompressionFlags.values()[flags]; + } + + public void setMasterSeed(byte[] masterSeed) { + this.masterSeed = masterSeed; + } + + public void setTransformSeed(byte[] transformSeed) { + this.transformSeed = transformSeed; + } + + public void setTransformRounds(long transformRounds) { + this.transformRounds = transformRounds; + } + + public void setEncryptionIv(byte[] encryptionIv) { + this.encryptionIv = encryptionIv; + } + + public void setInnerRandomStreamKey(byte[] key) { + this.innerRandomStreamKey = key; + } + + public void setStreamStartBytes(byte[] streamStartBytes) { + this.streamStartBytes = streamStartBytes; + } + + public void setInnerRandomStreamId(int innerRandomStreamId) { + this.protectedStreamAlgorithm = Encryption.ProtectedStreamAlgorithm.getAlgorithm(innerRandomStreamId); + } + + public void setProtectedStreamAlgorithm(Encryption.ProtectedStreamAlgorithm protectedStreamAlgorithm) { + this.protectedStreamAlgorithm = protectedStreamAlgorithm; + } + + public void setKeyDerivationFunction(KeyDerivationFunction keyDerivationFunction) { + this.keyDerivationFunction = keyDerivationFunction; + if (version > 3) { + kdfParameters = this.keyDerivationFunction.createKdfParameters(); + } + } + + public void setCipherAlgorithm(CipherAlgorithm cipherAlgorithm) { + this.cipherAlgorithm = cipherAlgorithm; + setCipherUuid(this.cipherAlgorithm.getCipherUuid()); + if (cipherAlgorithm.getName().equals("CHA_CHA_20")){ + encryptionIv = random.generateSeed(12); + } + } + public void setCipherUuid(byte[] uuid) { + ByteBuffer b = ByteBuffer.wrap(uuid); + UUID incoming = new UUID(b.getLong(), b.getLong(8)); + setCipherUuid(incoming); + this.cipherAlgorithm = Encryption.Cipher.getCipherAlgorithm(incoming); + } + + public void setCipherUuid(UUID uuid) { + if (!uuid.equals(Aes.getInstance().getCipherUuid()) && !uuid.equals(ChaCha.getInstance().getCipherUuid())) { + throw new IllegalStateException("Unknown Cipher UUID " + uuid); + } + this.cipherUuid = uuid; + } + + public void setHeaderHash(byte[] headerHash) { + this.headerHash = headerHash; + } + + public void setVersion(int version) { + if (!allowableVersions.contains(version)) { + throw new IllegalStateException("File version must be in " + allowableVersions); + } + this.version = version; + } + + /** + * V4 add Key Definition Function Parameters + */ + public void setKdfParameters(VariantDictionary kdfParameters) { + this.kdfParameters = kdfParameters; + this.keyDerivationFunction = Encryption.KeyDerivationFunction.getKdf(kdfParameters.get("$UUID").asUuid()); + } + + /** + * V4 Add custom data + */ + public void setCustomData(VariantDictionary customData) { + this.customData = customData; + } + + /** + * V4 add binary from inner header + */ + public void addBinary(byte[] bytes) { + binaries.add(bytes); + } + + public List getBinaries() { + return binaries; + } + + /** + * V4 provide access to the header as bytes for verification + */ + public byte[] getHeaderBytes() { + return headerBytes; + } + + /** + * V4 provide access to the header as bytes for verification + */ + public void setHeaderBytes(byte[] headerBytes) { + byte[] copy = new byte[headerBytes.length]; + System.arraycopy(headerBytes, 0, copy, 0, headerBytes.length); + this.headerBytes = copy; + } + // +} diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java index 505f0dd8..973fe4f5 100644 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxKeyFile.java @@ -16,44 +16,172 @@ package org.linguafranca.pwdb.kdbx; +import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.Hex; +import org.linguafranca.pwdb.security.Encryption; import org.w3c.dom.Document; +import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; +import java.io.FilterInputStream; +import java.io.IOException; import java.io.InputStream; +import java.io.PushbackInputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.DigestInputStream; +import java.util.Arrays; +import java.util.Objects; /** - * Class has a static method to load a key from a KDBX XML Key File - * - * @author jo + * Class has a static method to load a key from an {@link InputStream} */ @SuppressWarnings("WeakerAccess") public class KdbxKeyFile { - private static XPath xpath = XPathFactory.newInstance().newXPath(); + private static final XPath xpath = XPathFactory.newInstance().newXPath(); + private static final int BUFFER_SIZE = 65; + private static final int KEY_LEN_32 = 32; + private static final int KEY_LEN_64 = 64; /** - * Load a key from an InputStream with a KDBX XML key file. - * @param inputStream the input stream holding the key - * @return they key or null if there was a problem + * Load a key from an InputStream, in this method the inputStream represents the KeyFile + *

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

+ *

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

+ * Formats. KeePass supports the following key file formats: + *
    + *
  • XML (recommended, default). There is an XML format for key files. KeePass 2.x uses this format by + * default, i.e. when creating a key file in the master key dialog, an XML key file is created. The syntax + * and the semantics of the XML format allow to detect certain corruptions (especially such caused by faulty + * hardware or transfer problems), and a hash (in XML key files version 2.0 or higher) allows to verify the + * integrity of the key. This format is resistant to most encoding and new-line character changes (which is + * useful for instance when the user is opening and saving the key file or when transferring it from/to a + * server). Such a key file can be printed (as a backup on paper), and comments can be added in the file + * (with the usual XML syntax: ). It is the most flexible format; new features can be added + * easily in the future.
  • + *
  • 32 bytes. If the key file contains exactly 32 bytes, these are used as a 256-bit cryptographic key. + * This format requires the least disk space.
  • + *
  • Hexadecimal. If the key file contains exactly 64 hexadecimal characters (0-9 and A-F, + * in UTF-8/ASCII encoding, one line, no spaces), these are decoded to a 256-bit cryptographic key.
  • + *
  • Hashed. If a key file does not match any of the formats above, its content is hashed using a + * cryptographic hash function in order to build a key (typically a 256-bit key with SHA-256). + * This allows to use arbitrary files as key files.
  • + *
+ * @param inputStream the input stream holding the key, caller should close + * @return the key */ public static byte[] load(InputStream inputStream) { - String base64; + // wrap the stream to get its digest (in case we need it) + DigestInputStream digestInputStream = new DigestInputStream(inputStream, + Encryption.getSha256MessageDigestInstance()); + // wrap the stream, so we can test reading from it but then push back to get original stream + PushbackInputStream pis = new PushbackInputStream(digestInputStream, BUFFER_SIZE); + try { + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead = pis.read(buffer); + + // if length 32 assume binary key file + if (bytesRead == KEY_LEN_32) { + return Arrays.copyOf(buffer, bytesRead); + } + + // if length 64 may be hex encoded key file + if (bytesRead == KEY_LEN_64) { + try { + ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); + Charset charSet = StandardCharsets.UTF_8; + CharBuffer charBuffer = charSet.decode(byteBuffer); + charBuffer.limit(KEY_LEN_64); + char[] hexValue = new char[charBuffer.remaining()]; + charBuffer.get(hexValue); + return Hex.decodeHex(hexValue); // (avoid creating a String) + } catch (DecoderException ignored) { + // fall through it may be an XML file or just a file whose digest we want + } + } + // restore stream + pis.unread(buffer); + + // if length not 32 or 64 either an XML key file or just a file to get digest + try { + // see if it's an XML key file + return tryComputeXmlKeyFile(pis); + } catch (HashMismatchException e) { + throw new IllegalArgumentException("Invalid key in signature file"); + } catch (Exception ignored) { + // fall through to get file digest + } + + // is not a valid xml file, so read the remainder of file + byte[] sink = new byte[1024]; + // read file to get its digest + //noinspection StatementWithEmptyBody + while (digestInputStream.read(sink) > 0) { /* nothing */ } + return digestInputStream.getMessageDigest().digest(); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Read the InputStream (kdbx xml keyfile) and compute the hash (SHA-256) to build a key + * + * @param is The KeyFile as an InputStream, must return with stream open on error + * @return the computed byte array (keyFile) to compute the MasterKey + */ + private static byte[] tryComputeXmlKeyFile(InputStream is) throws HashMismatchException { + // DocumentBuilder closes input stream so wrap inputStream to inhibit this in case of failure + InputStream unCloseable = new FilterInputStream(is) { + @Override + public void close() { /* nothing */ } + }; try { DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - Document doc = documentBuilder.parse(inputStream); - base64 = (String) xpath.evaluate("//KeyFile/Key/Data/text()", doc, XPathConstants.STRING); - if (base64 == null) { - return null; + Document doc = documentBuilder.parse(unCloseable); + // get the key + String data = (String) xpath.evaluate("//KeyFile/Key/Data/text()", doc, XPathConstants.STRING); + if (data == null) { + throw new IllegalArgumentException("Key file does not contain a key"); + } + // get the file version + String version = (String) xpath.evaluate("//KeyFile/Meta/Version/text()", doc, XPathConstants.STRING); + // if not 2.0 then key is base64 encoded + if (Objects.isNull(version) || !version.equals("2.0")) { + return Base64.decodeBase64(data); } - } catch (Exception e) { - return null; + + // key data may contain white space + byte[] decodedData = Hex.decodeHex(data.replaceAll("\\s", "")); + byte[] decodedDataHash = Encryption.getSha256MessageDigestInstance().digest(decodedData); + + // hash used to verify the data + String hashToCheck = (String) xpath.evaluate("//KeyFile/Key/Data/@Hash", doc, XPathConstants.STRING); + byte[] decodedHashToCheck = Hex.decodeHex(hashToCheck); + + // hashToCheck is a truncated version of the actual hash + if (!Arrays.equals(Arrays.copyOf(decodedDataHash, decodedHashToCheck.length), decodedHashToCheck)) { + throw new HashMismatchException(); + } + return decodedData; + } catch (IOException | SAXException | ParserConfigurationException | XPathExpressionException | + DecoderException e) { + throw new IllegalArgumentException("An error occurred during XML parsing: " + e.getMessage()); } - // android compatibility - return Base64.decodeBase64(base64.getBytes()); } -} + private static class HashMismatchException extends Exception { + } +} \ No newline at end of file diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java new file mode 100644 index 00000000..98f36eab --- /dev/null +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxSerializer.java @@ -0,0 +1,630 @@ +/* + * Copyright 2015 Jo Rabin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx; + +import com.google.common.io.LittleEndianDataInputStream; +import com.google.common.io.LittleEndianDataOutputStream; +import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.hashedblock.*; +import org.linguafranca.pwdb.security.Encryption; +import org.linguafranca.pwdb.security.VariantDictionary; + +import javax.crypto.Mac; +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.security.DigestInputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Map; +import java.util.function.IntConsumer; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** + * This class provides static methods for the encryption and decryption of Keepass KDBX V3 and V4 files. + *

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

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

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

      + *

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

      + *

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

      + *

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

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

The methods in this class provide support for serializing and deserializing plain text payload content + * to and from the above encrypted format. + *

Various fields of the plain text XML (e.g. passwords) are additionally + * and optionally encrypted using a second encryption. They + * are stream encrypted, meaning they have to be decrypted in the + * same order as they were encrypted, namely actual XML document order. Or at least that + * is the way it seems. The methods of this class do not perform this aspect of encryption/decryption. + * @see this diagram + * @author jo + */ +@SuppressWarnings({"WeakerAccess", "UnstableApiUsage"}) +public class KdbxSerializer { + + // make entirely static + private KdbxSerializer() {} + + /** + * Provides the payload of a KDBX file as an unencrypted {@link InputStream}. + * @param credentials credentials for decryption of the stream + * @param kdbxHeader a KdbxHeader for the encryption parameters and so on + * @param inputStream a KDBX formatted input stream + * @return an unencrypted input stream, to be read and closed by the caller + * @throws IOException on error + */ + public static InputStream createUnencryptedInputStream(Credentials credentials, KdbxHeader kdbxHeader, InputStream inputStream) throws IOException { + // read plaintext outer header + readOuterHeader(inputStream, kdbxHeader); + + InputStream plainTextStream; + + if (kdbxHeader.getVersion() >= 4) { + // verify the header hashes + readOuterHeaderVerification(kdbxHeader, credentials, new DataInputStream(inputStream)); + // create an HMac Block input stream + HmacBlockInputStream hmacBlockInputStream = new HmacBlockInputStream(kdbxHeader.getHmacKey(credentials), inputStream, true); + // decrypt the contents of the blocks + plainTextStream = kdbxHeader.createDecryptedStream(credentials.getKey(), hmacBlockInputStream); + + } else {// V3 + // decrypt the input stream + InputStream decryptedInputStream = kdbxHeader.createDecryptedStream(credentials.getKey(), inputStream); + // the unencrypted stream starts with a byte pattern + checkStartBytes(kdbxHeader, decryptedInputStream); + // unencrypted stream consists of hashed blocks + plainTextStream = new HashedBlockInputStream(decryptedInputStream, true); + } + // need to decompress the stream + if (kdbxHeader.getCompressionFlags().equals(KdbxHeader.CompressionFlags.GZIP)) { + plainTextStream = new GZIPInputStream(plainTextStream); + } + // read inner header in V4 + if (kdbxHeader.getVersion() >= 4) { + readInnerHeader(kdbxHeader, plainTextStream); + } + // stream is now positioned on payload (assumed to be Keepass XML) + return plainTextStream; + } + + /** + * Provides an {@link OutputStream} to be encoded and encrypted in KDBX format + * + * @param credentials credentials for encryption of the stream + * @param kdbxHeader a KDBX header to control the formatting and encryption operation + * @param outputStream output stream to contain the KDBX formatted output + * @return an unencrypted output stream, to be written to, flushed and closed by the caller + * @throws IOException on error + */ + public static OutputStream createEncryptedOutputStream(Credentials credentials, KdbxHeader kdbxHeader, OutputStream outputStream) throws IOException { + // write plain text header + writeKdbxHeader(kdbxHeader, outputStream); + + OutputStream result; + + if (kdbxHeader.getVersion() >= 4) { + // write in plaintext + writeOuterHeaderVerification(kdbxHeader, credentials, new DataOutputStream(outputStream)); + // write encrypted blocks + OutputStream blockOutputStream = new HmacBlockOutputStream(kdbxHeader.getHmacKey(credentials), outputStream, true); + // accept plaintext + result = kdbxHeader.createEncryptedStream(credentials.getKey(), blockOutputStream); + + } else { + OutputStream encryptedOutputStream = kdbxHeader.createEncryptedStream(credentials.getKey(), outputStream); + // write encrypted + writeStartBytes(kdbxHeader, encryptedOutputStream); + // input is plaintext, output is encrypted after + result = new HashedBlockOutputStream(encryptedOutputStream, true); + } + + if(kdbxHeader.getCompressionFlags().equals(KdbxHeader.CompressionFlags.GZIP)) { + result = new GZIPOutputStream(result); + } + + if (kdbxHeader.getVersion() >= 4) { + writeInnerHeader(kdbxHeader, result); + } + + return result; + } + + /** + * Checks that the decrypted stream starts with the expected bytes in V3 format + * @param kdbxHeader the header + * @param decryptedInputStream the decrypted stream + * @throws IOException if the stream cannot be read, etc. + */ + private static void checkStartBytes(KdbxHeader kdbxHeader, InputStream decryptedInputStream) throws IOException { + LittleEndianDataInputStream ledis = new LittleEndianDataInputStream(decryptedInputStream); + + byte [] startBytes = new byte[32]; + ledis.readFully(startBytes); + if (!Arrays.equals(startBytes, kdbxHeader.getStreamStartBytes())) { + throw new IllegalStateException("Inconsistent stream start bytes. This usually means the credentials were wrong."); + } + } + + /** + * Writes the expected stream start bytes to the encrypted stream for V3 format + * @param kdbxHeader the header + * @param encryptedOutputStream the encrypted stream + * @throws IOException if the stream cannot be written, etc. + */ + private static void writeStartBytes(KdbxHeader kdbxHeader, OutputStream encryptedOutputStream) throws IOException { + LittleEndianDataOutputStream ledos = new LittleEndianDataOutputStream(encryptedOutputStream); + ledos.write(kdbxHeader.getStreamStartBytes()); + } + + private static final int SIG1 = 0x9AA2D903; + private static final int SIG2 = 0xB54BFB67; + private static final int FILE_VERSION_32 = 0x00030001; + private static final int FILE_VERSION_4 = 0x00040000; + + private static class HeaderType { + static final byte END = 0; + static final byte COMMENT = 1; + static final byte CIPHER_ID = 2; + static final byte COMPRESSION_FLAGS = 3; + static final byte MASTER_SEED = 4; + static final byte TRANSFORM_SEED = 5; + static final byte TRANSFORM_ROUNDS = 6; + static final byte ENCRYPTION_IV = 7; + static final byte INNER_RANDOM_STREAM_KEY = 8; + static final byte STREAM_START_BYTES = 9; + static final byte INNER_RANDOM_STREAM_ID = 10; + static final byte KDF_PARAMETERS = 11; + static final byte CUSTOM_DATA = 12; + } + + /** + * Create and populate a KdbxHeader from the input stream supplied + * @param inputStream an input stream + * @return the populated KdbxHeader + * @throws IOException on error + */ + public static KdbxHeader readOuterHeader(InputStream inputStream, KdbxHeader kdbxHeader) throws IOException { + + // header is digested to verify correctness + MessageDigest digest = Encryption.getSha256MessageDigestInstance(); + DigestInputStream shaDigestInputStream = new DigestInputStream(inputStream, digest); + // collect the bytes of the header, we'll need them later for the HMAC header calculation + // we can't use a similar technique to the digest stream above, since the + // HMac calculation depends on having collected a couple of the header fields + CollectingInputStream collectingInputStream = new CollectingInputStream(shaDigestInputStream, true); + // make values available from LittleEndian + LittleEndianDataInputStream ledis = new LittleEndianDataInputStream(collectingInputStream); + + // file starts with magic number + if (!verifyMagicNumber(ledis)) { + throw new IllegalStateException("Magic number did not match"); + } + // followed by a file version number + int fullVersion = ledis.readInt(); + kdbxHeader.setVersion(fullVersion >> 16); + + // read header fields + getOuterHeaderFields(kdbxHeader, ledis); + + // stop digesting + shaDigestInputStream.on(false); + // stop collecting the bytes of the header + collectingInputStream.setCollecting(false); + + kdbxHeader.setHeaderHash(digest.digest()); + kdbxHeader.setHeaderBytes(collectingInputStream.getCollectedBytes()); + + return kdbxHeader; + } + + /** + * V4 header is followed by an SHA256 and then contains an HMAC SHA256 after that. + * @param kdbxHeader the header containing the relevant parameters + * @param credentials the credentials - used to verify the HMAC + * @param input an input source + * @throws IOException on error + */ + public static void readOuterHeaderVerification(KdbxHeader kdbxHeader, Credentials credentials, DataInput input) throws IOException { + // check the SHA + byte [] receivedSha256 = getBytes(32, input); + if (!Arrays.equals(kdbxHeader.getHeaderHash(), receivedSha256)) { + throw new IllegalStateException("Header hash does not match"); + } + + byte[] hmacKey = kdbxHeader.getHmacKey(credentials); + // get the key for the header Hmac (using sequence number -1) + // KdbxFile.cs ComputeHeaderHmac + byte [] hmacKey64 = Encryption.transformHmacKey(hmacKey, Helpers.toBytes(-1L, ByteOrder.LITTLE_ENDIAN)); + Mac mac = Encryption.getHMacSha256Instance(hmacKey64); + byte [] computedHmacSha256 = mac.doFinal(kdbxHeader.getHeaderBytes()); + byte [] receivedHmacSha256 = getBytes(32, input); + if (!Arrays.equals(computedHmacSha256, receivedHmacSha256)) { + throw new IllegalStateException("Header HMAC does not match"); + } + } + + /** + * Write the hashes required after the header for V4 + * @param kdbxHeader the header whose hashes need to be written + * @param credentials database credentials + * @param dataOutputStream somewhere to write to + */ + + private static void writeOuterHeaderVerification(KdbxHeader kdbxHeader, Credentials credentials, DataOutputStream dataOutputStream) throws IOException { + // the SHA digest + dataOutputStream.write(kdbxHeader.getHeaderHash()); + + // the HMac + byte[] hmacKey = kdbxHeader.getHmacKey(credentials); + byte[] hmacKey64 = Encryption.transformHmacKey(hmacKey, Helpers.toBytes(-1L, ByteOrder.LITTLE_ENDIAN)); + Mac mac = Encryption.getHMacSha256Instance(hmacKey64); + byte[] hashedHeaderBytes = mac.doFinal(kdbxHeader.getHeaderBytes()); + dataOutputStream.write(hashedHeaderBytes); + } + + private static void getOuterHeaderFields(KdbxHeader kdbxHeader, DataInput input) throws IOException { + byte headerType; + do { + headerType = input.readByte(); + int length = (kdbxHeader.getVersion() == 3 ? input.readShort() : input.readInt()); + + switch (headerType) { + + case HeaderType.END: { + getBytes(length, input); + break; + } + + case HeaderType.COMMENT: + getBytes(length, input); + break; + + case HeaderType.CIPHER_ID: + kdbxHeader.setCipherUuid(getBytes(length, input)); + break; + + case HeaderType.COMPRESSION_FLAGS: + kdbxHeader.setCompressionFlags(getInt(length, input)); + break; + + case HeaderType.MASTER_SEED: + kdbxHeader.setMasterSeed(getBytes(length, input)); + break; + + case HeaderType.TRANSFORM_SEED: + kdbxHeader.setTransformSeed(getBytes(length, input)); + break; + + case HeaderType.TRANSFORM_ROUNDS: + kdbxHeader.setTransformRounds(getLong(length, input)); + break; + + case HeaderType.ENCRYPTION_IV: + kdbxHeader.setEncryptionIv(getBytes(length, input)); + break; + + case HeaderType.INNER_RANDOM_STREAM_KEY: + kdbxHeader.setInnerRandomStreamKey(getBytes(length, input)); + break; + + case HeaderType.STREAM_START_BYTES: + kdbxHeader.setStreamStartBytes(getBytes(length, input)); + break; + + case HeaderType.INNER_RANDOM_STREAM_ID: + kdbxHeader.setInnerRandomStreamId(getInt(length, input)); + break; + + case HeaderType.KDF_PARAMETERS: + kdbxHeader.setKdfParameters(readVariantDictionary(getBytes(length, input))); + break; + + case HeaderType.CUSTOM_DATA: + kdbxHeader.setCustomData(readVariantDictionary(getBytes(length, input))); + break; + + default: throw new IllegalStateException("Unknown File Header"); + } + } while (headerType != HeaderType.END); + } + + /** + * Write a KdbxHeader to the output stream supplied. The header is updated with the + * message digest of the written stream. + * @param kdbxHeader the header to write and update + * @throws IOException on error + */ + @SuppressWarnings("UnstableApiUsage") + public static void writeKdbxHeader(KdbxHeader kdbxHeader, OutputStream outputStream) throws IOException { + MessageDigest messageDigest = Encryption.getSha256MessageDigestInstance(); + DigestOutputStream digestOutputStream = new DigestOutputStream(outputStream, messageDigest); + CollectingOutputStream collectingOutputStream = new CollectingOutputStream(digestOutputStream); + LittleEndianDataOutputStream ledos = new LittleEndianDataOutputStream(collectingOutputStream); + + // lengths are short in v3 int in v4 + IntConsumer lengthWriter = (i -> { + try { + if (kdbxHeader.getVersion() == 3) { + ledos.writeShort(i); + } else { + ledos.writeInt(i); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + // write the magic number + ledos.writeInt(SIG1); + ledos.writeInt(SIG2); + // write a file version + ledos.writeInt(kdbxHeader.getVersion() == 3?FILE_VERSION_32:FILE_VERSION_4); + + ledos.writeByte(HeaderType.CIPHER_ID); + lengthWriter.accept(16); + byte[] b = new byte[16]; + ByteBuffer bb = ByteBuffer.wrap(b); + bb.putLong(kdbxHeader.getCipherUuid().getMostSignificantBits()); + bb.putLong(8, kdbxHeader.getCipherUuid().getLeastSignificantBits()); + ledos.write(b); + + ledos.writeByte(HeaderType.COMPRESSION_FLAGS); + lengthWriter.accept(4); + ledos.writeInt(kdbxHeader.getCompressionFlags().ordinal()); + + ledos.writeByte(HeaderType.MASTER_SEED); + lengthWriter.accept(kdbxHeader.getMasterSeed().length); + ledos.write(kdbxHeader.getMasterSeed()); + + if (kdbxHeader.getVersion() < 4) { + ledos.writeByte(HeaderType.TRANSFORM_SEED); + lengthWriter.accept(kdbxHeader.getTransformSeed().length); + ledos.write(kdbxHeader.getTransformSeed()); + + ledos.writeByte(HeaderType.TRANSFORM_ROUNDS); + lengthWriter.accept(8); + ledos.writeLong(kdbxHeader.getTransformRounds()); + } + + ledos.writeByte(HeaderType.ENCRYPTION_IV); + lengthWriter.accept(kdbxHeader.getEncryptionIv().length); + ledos.write(kdbxHeader.getEncryptionIv()); + + if (kdbxHeader.getVersion() < 4) { + ledos.writeByte(HeaderType.INNER_RANDOM_STREAM_KEY); + lengthWriter.accept(kdbxHeader.getInnerRandomStreamKey().length); + ledos.write(kdbxHeader.getInnerRandomStreamKey()); + + ledos.writeByte(HeaderType.STREAM_START_BYTES); + lengthWriter.accept(kdbxHeader.getStreamStartBytes().length); + ledos.write(kdbxHeader.getStreamStartBytes()); + + ledos.writeByte(HeaderType.INNER_RANDOM_STREAM_ID); + lengthWriter.accept(4); + ledos.writeInt(kdbxHeader.getProtectedStreamAlgorithm().ordinal()); + } + + if (kdbxHeader.getVersion() > 3) { + ledos.writeByte(HeaderType.KDF_PARAMETERS); + byte [] vd = serializeVariantDictionary(kdbxHeader.getKdfParameters()); + lengthWriter.accept(vd.length); + ledos.write(vd); + } + + ledos.writeByte(HeaderType.END); + lengthWriter.accept(0); + + MessageDigest digest = digestOutputStream.getMessageDigest(); + kdbxHeader.setHeaderHash(digest.digest()); + + collectingOutputStream.setCollecting(false); + kdbxHeader.setHeaderBytes(collectingOutputStream.getCollectedBytes()); + } + + /** + * Type fields for inner headers + * @see KdbxSerializer#readInnerHeader + */ + private static class InnerHeaderType { + private static final byte END = 0; + private static final byte INNER_RANDOM_STREAM_ID = 1; // Supersedes KdbxHeaderFieldID.InnerRandomStreamID + private static final byte INNER_RANDOM_STREAM_KEY = 2; // Supersedes KdbxHeaderFieldID.InnerRandomStreamKey + private static final byte BINARY = 3; + } + + /** + * From V4 the inner stream encryption parameters are contained in + * a set of headers immediately preceding the XML payload + * @param kdbxHeader the header whose values are to be read + * @param plainTextStream a stream to read them from + * @throws IOException on error + */ + private static void readInnerHeader(KdbxHeader kdbxHeader, InputStream plainTextStream) throws IOException { + DataInput input = new LittleEndianDataInputStream(plainTextStream); + + byte headerType; + do { + headerType = input.readByte(); + int length = input.readInt(); + + switch (headerType) { + case InnerHeaderType.END: { + getBytes(length, input); + break; + } + + case InnerHeaderType.INNER_RANDOM_STREAM_ID: { + kdbxHeader.setInnerRandomStreamId(getInt(length, input)); + break; + } + + case InnerHeaderType.INNER_RANDOM_STREAM_KEY: { + kdbxHeader.setInnerRandomStreamKey(getBytes(length, input)); + break; + } + + case InnerHeaderType.BINARY: { + kdbxHeader.addBinary(getBytes(length, input)); + break; + } + + default: throw new IllegalStateException("Invalid inner header field"); + } + } while (headerType != HeaderType.END); + } + + public static void writeInnerHeader(KdbxHeader kdbxHeader, OutputStream outputStream) throws IOException { + DataOutput output = new LittleEndianDataOutputStream(outputStream); + + output.writeByte(InnerHeaderType.INNER_RANDOM_STREAM_ID); + output.writeInt(4); + output.writeInt(kdbxHeader.getProtectedStreamAlgorithm().ordinal()); + + output.writeByte(InnerHeaderType.INNER_RANDOM_STREAM_KEY); + output.writeInt(kdbxHeader.getInnerRandomStreamKey().length); + output.write(kdbxHeader.getInnerRandomStreamKey()); + + for (byte [] binary: kdbxHeader.getBinaries()) { + output.writeByte(InnerHeaderType.BINARY); + output.writeInt(binary.length); + output.write(binary); + } + + output.writeByte(InnerHeaderType.END); + output.writeInt(0); + } + + /** + * Read a VariantDictionary from the supplied input according to KDBX rules + * @param source source of data + * @return a VariantDictionary + */ + public static VariantDictionary readVariantDictionary(byte [] source) { + // read the buffer containing the dictionary, which starts with a 4 byte length + ByteBuffer buf = ByteBuffer.wrap(source); + buf.order(ByteOrder.LITTLE_ENDIAN); + // version number must be 0x01?? + + VariantDictionary vd = new VariantDictionary((short) (buf.getShort() >> 8)); + + // sequence of entries followed by a byte 0 + byte type = buf.get(); + while (type != 0) { + // get key + int keyLength = buf.getInt(); + byte [] key = new byte[keyLength]; + buf.get(key); + + // get value + int valueLength = buf.getInt(); + byte [] value = new byte[valueLength]; + buf.get(value); + + // add entry + vd.put(new String(key, StandardCharsets.US_ASCII), VariantDictionary.EntryType.get(type), value); + + type = buf.get(); + } + return vd; + } + + /** + * Serialize a variant dictionary according to KDBX rules + * @param v the dictionary to serialize + * @return a byte array + */ + public static byte[] serializeVariantDictionary(VariantDictionary v) { + ByteBuffer buf = ByteBuffer.wrap(new byte[1024]); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.mark(); + // version number must be 0x01?? + buf.putShort((short) 0x100); //1 + for (Map.Entry e : v.getEntries().entrySet()){ + buf.put(e.getValue().getType()); + buf.putInt(e.getKey().length()); + buf.put(e.getKey().getBytes(StandardCharsets.US_ASCII)); + buf.putInt(e.getValue().asByteArray().length); + buf.put(e.getValue().asByteArray()); + } + buf.put((byte) 0); + byte[] result = new byte[buf.position()]; + buf.reset(); + buf.get(result); + return result; + } + + /** + * Read two lots of 4 bytes and verify that they satisfy the signature of a kdbx file; + * @param ledis an input stream + * @return true if it looks like this is a kdbx file + * @throws IOException on error + */ + private static boolean verifyMagicNumber(LittleEndianDataInputStream ledis) throws IOException { + int sig1 = ledis.readInt(); + int sig2 = ledis.readInt(); + return sig1 == SIG1 && sig2 == SIG2; + } + + private static int getInt(int length, DataInput input) throws IOException { + if (length != 4) { + throw new IllegalStateException("Int required but length was " + length); + } + return input.readInt(); + } + + private static long getLong(int length, DataInput input) throws IOException { + if (length != 8) { + throw new IllegalStateException("Long required but length was " + length); + } + return input.readLong(); + } + + private static byte[] getBytes(int numBytes, DataInput input) throws IOException { + byte [] result = new byte[numBytes]; + input.readFully(result); + return result; + } +} diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java new file mode 100644 index 00000000..50f95041 --- /dev/null +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/KdbxStreamFormat.java @@ -0,0 +1,132 @@ +/* + * 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.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.SerializableDatabase; +import org.linguafranca.pwdb.StreamFormat; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; + +/** + * This class implements KDBX formatted saving and loading of databases + */ +public class KdbxStreamFormat implements StreamFormat { + + private KdbxHeader kdbxHeader; + /** + * Create a StreamFormat for reading or for writing v3 with default KdbxHeader + */ + public KdbxStreamFormat() { + this(new KdbxHeader(3)); + } + + /** + * Specify a version for writing using default KdbxHeader + * + * @param version the version + * @deprecated use {@link #KdbxStreamFormat(KdbxHeader)} with version set + */ + @Deprecated + public KdbxStreamFormat(Version version) { + this(version.getVersionNum() == 3? new KdbxHeader(): new KdbxHeader(4)); + } + + /** + * Provide a {@link KdbxHeader} which includes version info + * @param kdbxHeader a {@link KdbxHeader} to specify (write) or (capture) read the file config + */ + + public KdbxStreamFormat(KdbxHeader kdbxHeader) { + this.kdbxHeader = kdbxHeader; + } + + @Override + public void load(SerializableDatabase serializableDatabase, Credentials credentials, InputStream encryptedInputStream) throws IOException { + try (InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, kdbxHeader, encryptedInputStream)) { + serializableDatabase.setEncryption(kdbxHeader.getStreamEncryptor()); + serializableDatabase.load(decryptedInputStream); + if (kdbxHeader.getVersion() == 3 && !Arrays.equals(serializableDatabase.getHeaderHash(), kdbxHeader.getHeaderHash())) { + throw new IllegalStateException("Header hash does not match"); + } + if (kdbxHeader.getVersion() == 4) { + int count = 0; + for (byte[] binary : kdbxHeader.getBinaries()) { + serializableDatabase.addBinary(count, Arrays.copyOfRange(binary, 1, binary.length)); + count++; + } + } + } + } + + @Override + public void save(SerializableDatabase serializableDatabase, Credentials credentials, OutputStream outputStream) throws IOException { + Helpers.isV4.set(kdbxHeader.getVersion() == 4); + if (kdbxHeader.getVersion() == 4) { + for (int a = 0; a < serializableDatabase.getBinaryCount(); a++) { + int attachmentLength = serializableDatabase.getBinary(a).length; + byte[] binary = new byte[attachmentLength + 1]; + binary[0] = 0; + System.arraycopy(serializableDatabase.getBinary(a),0, binary, 1, attachmentLength); + kdbxHeader.addBinary(binary); + } + // TODO the binaries should now be removed so they don't get serialized in XML + } + + try (OutputStream encryptedOutputStream = KdbxSerializer.createEncryptedOutputStream(credentials, kdbxHeader, outputStream)) { + if (kdbxHeader.getVersion() == 3) { + serializableDatabase.setHeaderHash(kdbxHeader.getHeaderHash()); + } + serializableDatabase.setEncryption(kdbxHeader.getStreamEncryptor()); + serializableDatabase.save(encryptedOutputStream); + encryptedOutputStream.flush(); + } + } + + @Override + public KdbxHeader getStreamConfiguration() { + return kdbxHeader; + } + + @Override + public void setStreamConfiguration(KdbxHeader configuration) { + this.kdbxHeader = configuration; + } + + /** + * KDBX file format specifier + * @deprecated use constructor with {@link KdbxHeader#setVersion(int)} + */ + @Deprecated + public enum Version { + KDBX31(3), + KDBX4(4); + + private final int version; + + Version(int num) { + this.version = num; + } + + int getVersionNum() { + return this.version; + } + } +} diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamEncryptor.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamEncryptor.java deleted file mode 100644 index b624dbf8..00000000 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/StreamEncryptor.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.pwdb.kdbx; - -/** - * 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; - } - } -} diff --git a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java similarity index 72% rename from dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java rename to kdbx/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java index 53558d31..7fcb9f70 100644 --- a/dom/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java +++ b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/dom/DomHelper.java @@ -23,13 +23,11 @@ import org.w3c.dom.Element; import org.w3c.dom.NodeList; -//import javax.xml.bind.DatatypeConverter; - import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; -import java.text.SimpleDateFormat; +import java.time.Instant; import java.util.*; /** @@ -37,11 +35,9 @@ * * @author jo */ -class DomHelper { - - static XPath xpath = XPathFactory.newInstance().newXPath(); +public class DomHelper { - static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + public static XPath xpath = XPathFactory.newInstance().newXPath(); static final String GROUP_ELEMENT_NAME = "Group"; static final String ENTRY_ELEMENT_NAME = "Entry"; @@ -66,6 +62,10 @@ class DomHelper { static final String BINARY_PROPERTY_ELEMENT_FORMAT = "Binary[Key/text()='%s']"; static final String VALUE_ELEMENT_NAME = "Value"; + static final String RECYCLE_BIN_UUID_ELEMENT_NAME = "RecycleBinUuid"; + static final String RECYCLE_BIN_ENABLED_ELEMENT_NAME = "RecycleBinEnabled"; + static final String RECYCLE_BIN_CHANGED_ELEMENT_NAME = "RecycleBinChanged"; + interface ValueCreator { String getValue(); } @@ -84,7 +84,7 @@ public String getValue() { static class DateValueCreator implements ValueCreator { @Override public String getValue() { - return dateFormatter.format(new Date()); + return Helpers.fromDate(Date.from(Instant.now())); } } @@ -116,6 +116,16 @@ static Element getElement(String elementPath, Element parentElement, boolean cr } } + static boolean removeElement(String elementPath, Element parentElement) { + Element toRemove = getElement(elementPath, parentElement, false); + if (toRemove == null) { + return false; + } else { + toRemove.getParentNode().removeChild(toRemove); + return true; + } + } + static List getElements (String elementPath, Element parentElement) { try { NodeList nodes = (NodeList) xpath.evaluate(elementPath, parentElement, XPathConstants.NODESET); @@ -138,6 +148,7 @@ static int getElementsCount (String elementPath, Element parentElement) { } } + @Nullable static Element newElement(String elementName, Element parentElement) { Element newElement = parentElement.getOwnerDocument().createElement(elementName); parentElement.appendChild(newElement); @@ -145,23 +156,23 @@ static Element newElement(String elementName, Element parentElement) { } @Nullable - static String getElementContent(String elementPath, Element parentElement) { + public static String getElementContent(String elementPath, Element parentElement) { Element result = getElement(elementPath, parentElement, false); return (result == null) ? null : result.getTextContent(); } @NotNull - static String ensureElementContent(String elementPath, Element parentElement, @NotNull String value) { + public static String ensureElementContent(String elementPath, Element parentElement, @NotNull String value) { Element result = getElement(elementPath, parentElement, false); if (result == null) { result = createHierarchically(elementPath, parentElement); - result.setTextContent(value); } + result.setTextContent(value); return result.getTextContent(); } @NotNull - static Element setElementContent(String elementPath, Element parentElement, String value) { + public static Element setElementContent(String elementPath, Element parentElement, String value) { Element result = getElement(elementPath, parentElement, true); result.setTextContent(value); return result; @@ -188,16 +199,12 @@ static Element setBinaryElementContent(String elementPath, Element parentElement //Find the highest numbered existing content String max = xpath.evaluate("//Binaries/Binary/@ID[not(. < ../../Binary/@ID)][1]", parentElement.getOwnerDocument().getDocumentElement()); + if (max.trim().isEmpty()){ + max = "-1"; + } Integer newIndex = Integer.valueOf(max) + 1; - Element binaries = getElement("//Binaries", parentElement.getOwnerDocument().getDocumentElement(),false); - if (binaries == null) { - throw new IllegalStateException("Binaries not found"); - } - Element binary = (Element) binaries.appendChild(binaries.getOwnerDocument().createElement("Binary")); - binary.setTextContent(b64); - binary.setAttribute("Compressed", "True"); - binary.setAttribute("ID", newIndex.toString()); + addBinary(parentElement.getOwnerDocument().getDocumentElement(), b64, newIndex); Element result = getElement(elementPath, parentElement, true); result.setAttribute("Ref", newIndex.toString()); @@ -210,10 +217,45 @@ static Element setBinaryElementContent(String elementPath, Element parentElement } } + /** + * Add a binary property value to the V3 Meta/Binaries element + * + * @param documentElement the document element + * @param b64 a base64 gzipped encoded representation of the binary content + * @param index the index by which it is known + */ + public static void addBinary(Element documentElement, String b64, Integer index) { + Element binaries = getElement("Meta/Binaries", documentElement,true); + Element binary = (Element) binaries.appendChild(binaries.getOwnerDocument().createElement("Binary")); + binary.setTextContent(b64); + binary.setAttribute("Compressed", "True"); + binary.setAttribute("ID", index.toString()); + } + + public static String getBinary(Element documentElement, Integer index) { + Element binaries = getElement("Meta/Binaries", documentElement,false); + if (Objects.isNull(binaries)){ + throw new IllegalArgumentException("No binaries found"); + } + for (int i = 0; i < binaries.getChildNodes().getLength(); i++){ + if (((Element) binaries.getChildNodes().item(i)).getAttribute("ID").equals(index.toString())) { + return ((Element) binaries.getChildNodes().item(i)).getTextContent(); + } + } + throw new IllegalArgumentException("No binary with that index found"); + } + + public static int getBinaryCount(Element documentElement) { + Element binaries = getElement("Meta/Binaries", documentElement,false); + if (Objects.isNull(binaries)) { + return 0; + } + return binaries.getChildNodes().getLength(); + } @NotNull static Element touchElement(String elementPath, Element parentElement) { - return setElementContent(elementPath, parentElement, dateFormatter.format(new Date())); + return setElementContent(elementPath, parentElement, Helpers.fromDate(new Date())); } private static Element createHierarchically(String elementPath, Element startElement) { diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxHeader.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxHeader.java deleted file mode 100644 index bbfef5d1..00000000 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxHeader.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.pwdb.kdbx.stream_3_1; - -import org.linguafranca.pwdb.kdbx.StreamFormat; -import org.linguafranca.pwdb.security.Encryption; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.security.SecureRandom; -import java.util.UUID; - -/** - * This class represents the header portion of a KeePass KDBX file or stream. The header is received in - * plain text and describes the encryption and compression of the remainder of the file. - * - *

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

While KDBX streams are Little-Endian, data is passed to and from this class in standard Java byte order. - * - * @author jo - */ -@SuppressWarnings("WeakerAccess") -public class KdbxHeader { - - /** - * The ordinal 0 represents uncompressed and 1 GZip compressed - */ - @SuppressWarnings("WeakerAccess") - public enum CompressionFlags { - NONE, GZIP - } - - /** - * The ordinals represent various types of encryption that may - * be applied to fields within the unencrypted data - * - * @see StreamFormat - * @see KdbxStreamFormat - */ - @SuppressWarnings("WeakerAccess, unused") - public enum ProtectedStreamAlgorithm { - NONE, ARC_FOUR, SALSA_20 - } - - /** - * This UUID denotes that AES Cipher is in use. No other values are known. - */ - public static final UUID AES_CIPHER = UUID.fromString("31C1F2E6-BF71-4350-BE58-05216AFC5AFF"); - - /* the cipher in use */ - private UUID cipherUuid; - /* whether the data is compressed */ - private CompressionFlags compressionFlags; - private byte [] masterSeed; - private byte[] transformSeed; - private long transformRounds; - private byte[] encryptionIv; - private byte[] protectedStreamKey; - private ProtectedStreamAlgorithm protectedStreamAlgorithm; - /* these bytes appear in cipher text immediately following the header */ - private byte[] streamStartBytes; - /* not transmitted as part of the header, used in the XML payload, so calculated - * on transmission or receipt */ - private byte[] headerHash; - - /** - * Construct a default KDBX header - */ - public KdbxHeader() { - SecureRandom random = new SecureRandom(); - cipherUuid = AES_CIPHER; - compressionFlags = CompressionFlags.GZIP; - masterSeed = random.generateSeed(32); - transformSeed = random.generateSeed(32); - transformRounds = 6000; - encryptionIv = random.generateSeed(16); - protectedStreamKey = random.generateSeed(32); - streamStartBytes = new byte[32]; - protectedStreamAlgorithm = ProtectedStreamAlgorithm.SALSA_20; - } - - /** - * Create a decrypted input stream using supplied digest and this header - * apply decryption to the passed encrypted input stream - * - * @param digest the key digest - * @param inputStream the encrypted input stream - * @return a decrypted stream - * @throws IOException if something bad happens - */ - public InputStream createDecryptedStream(byte[] digest, InputStream inputStream) throws IOException { - byte[] finalKeyDigest = Encryption.getFinalKeyDigest(digest, getMasterSeed(), getTransformSeed(), getTransformRounds()); - return Encryption.getDecryptedInputStream(inputStream, finalKeyDigest, getEncryptionIv()); - } - - /** - * Create an unencrypted outputstream using the supplied digest and this header - * and use the supplied output stream to write encrypted data. - * @param digest the key digest - * @param outputStream the output stream which is the destination for encrypted data - * @return an output stream to write unencrypted data to - * @throws IOException if something bad happens - */ - public OutputStream createEncryptedStream(byte[] digest, OutputStream outputStream) throws IOException { - byte[] finalKeyDigest = Encryption.getFinalKeyDigest(digest, getMasterSeed(), getTransformSeed(), getTransformRounds()); - return Encryption.getEncryptedOutputStream(outputStream, finalKeyDigest, getEncryptionIv()); - } - - public UUID getCipherUuid() { - return cipherUuid; - } - - public CompressionFlags getCompressionFlags() { - return compressionFlags; - } - - public byte[] getMasterSeed() { - return masterSeed; - } - - public byte[] getTransformSeed() { - return transformSeed; - } - - public long getTransformRounds() { - return transformRounds; - } - - public byte[] getEncryptionIv() { - return encryptionIv; - } - - public byte[] getProtectedStreamKey() { - return protectedStreamKey; - } - - public byte[] getStreamStartBytes() { - return streamStartBytes; - } - - public ProtectedStreamAlgorithm getProtectedStreamAlgorithm() { - return protectedStreamAlgorithm; - } - - public byte[] getHeaderHash() { - return headerHash; - } - - public void setCipherUuid(byte[] uuid) { - ByteBuffer b = ByteBuffer.wrap(uuid); - UUID incoming = new UUID(b.getLong(), b.getLong(8)); - if (!incoming.equals(AES_CIPHER)) { - throw new IllegalStateException("Unknown Cipher UUID " + incoming.toString()); - } - this.cipherUuid = incoming; - } - - public void setCompressionFlags(int flags) { - this.compressionFlags = CompressionFlags.values()[flags]; - } - - public void setMasterSeed(byte[] masterSeed) { - this.masterSeed = masterSeed; - } - - public void setTransformSeed(byte[] transformSeed) { - this.transformSeed = transformSeed; - } - - public void setTransformRounds(long transformRounds) { - this.transformRounds = transformRounds; - } - - public void setEncryptionIv(byte[] encryptionIv) { - this.encryptionIv = encryptionIv; - } - - public void setProtectedStreamKey(byte[] protectedStreamKey) { - this.protectedStreamKey = protectedStreamKey; - } - - public void setStreamStartBytes(byte[] streamStartBytes) { - this.streamStartBytes = streamStartBytes; - } - - public void setInnerRandomStreamId(int innerRandomStreamId) { - this.protectedStreamAlgorithm = ProtectedStreamAlgorithm.values()[innerRandomStreamId]; - } - - public void setHeaderHash(byte[] headerHash) { - this.headerHash = headerHash; - } -} diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.java deleted file mode 100644 index 108fe1cb..00000000 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.java +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.pwdb.kdbx.stream_3_1; - -import com.google.common.io.LittleEndianDataInputStream; -import com.google.common.io.LittleEndianDataOutputStream; -import org.linguafranca.pwdb.hashedblock.HashedBlockInputStream; -import org.linguafranca.pwdb.hashedblock.HashedBlockOutputStream; -import org.linguafranca.pwdb.Credentials; -import org.linguafranca.pwdb.security.Encryption; - -import java.io.*; -import java.nio.ByteBuffer; -import java.security.DigestInputStream; -import java.security.DigestOutputStream; -import java.security.MessageDigest; -import java.util.Arrays; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - -/** - * This class provides static methods for the encryption and decryption of Keepass KDBX files. - *

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

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

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

      - *

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

      - *
    - *
  3. An encrypted portion
  4. - *
      - *
    1. A sequence of bytes contained in the header. If they don't match, decryption has not worked.
    2. - *
    3. A payload serialized in Hashed Block format, see e.g. {@link HashedBlockInputStream} for details of this.
    4. - *
    5. The content of this payload may be GZIP compressed.
    6. - *
    7. The content is now a character stream, which is expected to be - * XML representing a KeePass Database. Assumed UTF-8 encoding.
    8. - *
    - *
- *

The methods in this class provide support for serializing and deserializing plain text payload content - * to and from the above encrypted format. - *

Various fields of the plain text XML (e.g. passwords) are additionally - * and optionally encrypted using a second encryption. They - * are stream encrypted, meaning they have to be decrypted in the - * same order as they were encrypted, namely actual XML document order. Or at least that - * is the way it seems. The methods of this class do not perform this aspect of encryption/decryption. - * @author jo - */ -@SuppressWarnings("WeakerAccess") -public class KdbxSerializer { - - // make entirely static - private KdbxSerializer() {} - - /** - * Provides the payload of a KDBX file as an unencrypted {@link InputStream}. - * @param credentials credentials for decryption of the stream - * @param kdbxHeader a header instance to be populated with values from the stream - * @param inputStream a KDBX formatted input stream - * @return an unencrypted input stream, to be read and closed by the caller - * @throws IOException on error - */ - public static InputStream createUnencryptedInputStream(Credentials credentials, KdbxHeader kdbxHeader, InputStream inputStream) throws IOException { - - readKdbxHeader(kdbxHeader, inputStream); - - InputStream decryptedInputStream = kdbxHeader.createDecryptedStream(credentials.getKey(), inputStream); - - checkStartBytes(kdbxHeader, decryptedInputStream); - - HashedBlockInputStream blockInputStream = new HashedBlockInputStream(decryptedInputStream, true); - - if (kdbxHeader.getCompressionFlags().equals(KdbxHeader.CompressionFlags.NONE)) { - return blockInputStream; - } - return new GZIPInputStream(blockInputStream); - } - - /** - * Provides an {@link OutputStream} to be encoded and encrypted in KDBX format - * @param credentials credentials for encryption of the stream - * @param kdbxHeader a KDBX header to control the formatting and encryption operation - * @param outputStream output stream to contain the KDBX formatted output - * @return an unencrypted output stream, to be written to, flushed and closed by the caller - * @throws IOException on error - */ - public static OutputStream createEncryptedOutputStream(Credentials credentials, KdbxHeader kdbxHeader, OutputStream outputStream) throws IOException { - - writeKdbxHeader(kdbxHeader, outputStream); - - OutputStream encryptedOutputStream = kdbxHeader.createEncryptedStream(credentials.getKey(), outputStream); - - writeStartBytes(kdbxHeader, encryptedOutputStream); - - HashedBlockOutputStream blockOutputStream = new HashedBlockOutputStream(encryptedOutputStream, true); - - if(kdbxHeader.getCompressionFlags().equals(KdbxHeader.CompressionFlags.NONE)) { - return blockOutputStream; - } - return new GZIPOutputStream(blockOutputStream); - } - - - private static void checkStartBytes(KdbxHeader kdbxHeader, InputStream decryptedInputStream) throws IOException { - LittleEndianDataInputStream ledis = new LittleEndianDataInputStream(decryptedInputStream); - - byte [] startBytes = new byte[32]; - ledis.readFully(startBytes); - if (!Arrays.equals(startBytes, kdbxHeader.getStreamStartBytes())) { - throw new IllegalStateException("Inconsistent stream start bytes. This usually means the credentials were wromng."); - } - } - - private static void writeStartBytes(KdbxHeader kdbxHeader, OutputStream encryptedOutputStream) throws IOException { - LittleEndianDataOutputStream ledos = new LittleEndianDataOutputStream(encryptedOutputStream); - ledos.write(kdbxHeader.getStreamStartBytes()); - } - - private static final int SIG1 = 0x9AA2D903; - private static final int SIG2 = 0xB54BFB67; - private static final int FILE_VERSION_CRITICAL_MASK = 0xFFFF0000; - private static final int FILE_VERSION_32 = 0x00030001; - - private static class HeaderType { - static final byte END = 0; - static final byte COMMENT = 1; - static final byte CIPHER_ID = 2; - static final byte COMPRESSION_FLAGS = 3; - static final byte MASTER_SEED = 4; - static final byte TRANSFORM_SEED = 5; - static final byte TRANSFORM_ROUNDS = 6; - static final byte ENCRYPTION_IV = 7; - static final byte PROTECTED_STREAM_KEY = 8; - static final byte STREAM_START_BYTES = 9; - static final byte INNER_RANDOM_STREAM_ID = 10; - } - - /** - * Read two lots of 4 bytes and verify that they satisfy the signature of a - * kdbx file; - * @param ledis an input stream - * @return true if it looks like this is a kdbx file - * @throws IOException on error - */ - private static boolean verifyMagicNumber(LittleEndianDataInputStream ledis) throws IOException { - int sig1 = ledis.readInt(); - int sig2 = ledis.readInt(); - return sig1 == SIG1 && sig2 == SIG2; - } - - /** - * Read 4 bytes and make sure they conform to expectations of file version - * @param ledis an input stream - * @return true if it looks like we understand this file version - * @throws IOException on error - */ - private static boolean verifyFileVersion(LittleEndianDataInputStream ledis) throws IOException { - return ((ledis.readInt() & FILE_VERSION_CRITICAL_MASK) <= (FILE_VERSION_32 & FILE_VERSION_CRITICAL_MASK)); - } - - /** - * Populate a KdbxHeader from the input stream supplied - * @param kdbxHeader a header to be populated - * @param inputStream an input stream - * @return the populated KdbxHeader - * @throws IOException on error - */ - public static KdbxHeader readKdbxHeader(KdbxHeader kdbxHeader, InputStream inputStream) throws IOException { - - MessageDigest digest = Encryption.getMessageDigestInstance(); - // we do not close this stream, otherwise we lose our place in the underlying stream - DigestInputStream digestInputStream = new DigestInputStream(inputStream, digest); - // we do not close this stream, otherwise we lose our place in the underlying stream - LittleEndianDataInputStream ledis = new LittleEndianDataInputStream(digestInputStream); - - if (!verifyMagicNumber(ledis)) { - throw new IllegalStateException("Magic number did not match"); - } - - if (!verifyFileVersion(ledis)) { - throw new IllegalStateException("File version did not match"); - } - - byte headerType; - while ((headerType = ledis.readByte()) != HeaderType.END) { - switch (headerType) { - - case HeaderType.COMMENT: - getByteArray(ledis); - break; - - case HeaderType.CIPHER_ID: - kdbxHeader.setCipherUuid(getByteArray(ledis)); - break; - - case HeaderType.COMPRESSION_FLAGS: - kdbxHeader.setCompressionFlags(getInt(ledis)); - break; - - case HeaderType.MASTER_SEED: - kdbxHeader.setMasterSeed(getByteArray(ledis)); - break; - - case HeaderType.TRANSFORM_SEED: - kdbxHeader.setTransformSeed(getByteArray(ledis)); - break; - - case HeaderType.TRANSFORM_ROUNDS: - kdbxHeader.setTransformRounds(getLong(ledis)); - break; - - case HeaderType.ENCRYPTION_IV: - kdbxHeader.setEncryptionIv(getByteArray(ledis)); - break; - - case HeaderType.PROTECTED_STREAM_KEY: - kdbxHeader.setProtectedStreamKey(getByteArray(ledis)); - break; - - case HeaderType.STREAM_START_BYTES: - kdbxHeader.setStreamStartBytes(getByteArray(ledis)); - break; - - case HeaderType.INNER_RANDOM_STREAM_ID: - kdbxHeader.setInnerRandomStreamId(getInt(ledis)); - break; - - default: throw new IllegalStateException("Unknown File Header"); - } - } - - // consume length etc. following END flag - getByteArray(ledis); - - kdbxHeader.setHeaderHash(digest.digest()); - return kdbxHeader; - } - - /** - * Write a KdbxHeader to the output stream supplied. The header is updated with the - * message digest of the written stream. - * @param kdbxHeader the header to write and update - * @param outputStream the output stream - * @throws IOException on error - */ - public static void writeKdbxHeader(KdbxHeader kdbxHeader, OutputStream outputStream) throws IOException { - MessageDigest messageDigest = Encryption.getMessageDigestInstance(); - DigestOutputStream digestOutputStream = new DigestOutputStream(outputStream, messageDigest); - LittleEndianDataOutputStream ledos = new LittleEndianDataOutputStream(digestOutputStream); - - // write the magic number - ledos.writeInt(SIG1); - ledos.writeInt(SIG2); - // write a file version - ledos.writeInt(FILE_VERSION_32); - - ledos.writeByte(HeaderType.CIPHER_ID); - ledos.writeShort(16); - byte[] b = new byte[16]; - ByteBuffer bb = ByteBuffer.wrap(b); - bb.putLong(kdbxHeader.getCipherUuid().getMostSignificantBits()); - bb.putLong(8, kdbxHeader.getCipherUuid().getLeastSignificantBits()); - ledos.write(b); - - ledos.writeByte(HeaderType.COMPRESSION_FLAGS); - ledos.writeShort(4); - ledos.writeInt(kdbxHeader.getCompressionFlags().ordinal()); - - ledos.writeByte(HeaderType.MASTER_SEED); - ledos.writeShort(kdbxHeader.getMasterSeed().length); - ledos.write(kdbxHeader.getMasterSeed()); - - ledos.writeByte(HeaderType.TRANSFORM_SEED); - ledos.writeShort(kdbxHeader.getTransformSeed().length); - ledos.write(kdbxHeader.getTransformSeed()); - - ledos.writeByte(HeaderType.TRANSFORM_ROUNDS); - ledos.writeShort(8); - ledos.writeLong(kdbxHeader.getTransformRounds()); - - ledos.writeByte(HeaderType.ENCRYPTION_IV); - ledos.writeShort(kdbxHeader.getEncryptionIv().length); - ledos.write(kdbxHeader.getEncryptionIv()); - - ledos.writeByte(HeaderType.PROTECTED_STREAM_KEY); - ledos.writeShort(kdbxHeader.getProtectedStreamKey().length); - ledos.write(kdbxHeader.getProtectedStreamKey()); - - ledos.writeByte(HeaderType.STREAM_START_BYTES); - ledos.writeShort(kdbxHeader.getStreamStartBytes().length); - ledos.write(kdbxHeader.getStreamStartBytes()); - - ledos.writeByte(HeaderType.INNER_RANDOM_STREAM_ID); - ledos.writeShort(4); - ledos.writeInt(kdbxHeader.getProtectedStreamAlgorithm().ordinal()); - - ledos.writeByte(HeaderType.END); - ledos.writeShort(0); - - MessageDigest digest = digestOutputStream.getMessageDigest(); - kdbxHeader.setHeaderHash(digest.digest()); - } - - - private static int getInt(LittleEndianDataInputStream ledis) throws IOException { - short fieldLength = ledis.readShort(); - if (fieldLength != 4) { - throw new IllegalStateException("Int required but length was " + fieldLength); - } - return ledis.readInt(); - } - - private static long getLong(LittleEndianDataInputStream ledis) throws IOException { - short fieldLength = ledis.readShort(); - if (fieldLength != 8) { - throw new IllegalStateException("Long required but length was " + fieldLength); - } - return ledis.readLong(); - } - - private static byte [] getByteArray(LittleEndianDataInputStream ledis) throws IOException { - short fieldLength = ledis.readShort(); - byte [] value = new byte[fieldLength]; - ledis.readFully(value); - return value; - } -} diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxStreamFormat.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxStreamFormat.java deleted file mode 100644 index 62aa4398..00000000 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxStreamFormat.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.pwdb.kdbx.stream_3_1; - -import org.linguafranca.pwdb.kdbx.SerializableDatabase; -import org.linguafranca.pwdb.kdbx.StreamFormat; -import org.linguafranca.pwdb.Credentials; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -/** - * This class implements KDBX formatted saving and loading of databases - * - * @author jo - */ -public class KdbxStreamFormat implements StreamFormat { - - @Override - public void load(SerializableDatabase serializableDatabase, Credentials credentials, InputStream encryptedInputStream) throws IOException { - KdbxHeader kdbxHeader = new KdbxHeader(); - InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, kdbxHeader, encryptedInputStream); - serializableDatabase.setEncryption(new Salsa20StreamEncryptor(kdbxHeader.getProtectedStreamKey())); - serializableDatabase.load(decryptedInputStream); - decryptedInputStream.close(); - } - - @Override - public void save(SerializableDatabase serializableDatabase, Credentials credentials, OutputStream encryptedOutputStream) throws IOException { - // fresh kdbx header - KdbxHeader kdbxHeader = new KdbxHeader(); - OutputStream unencrytedOutputStream = KdbxSerializer.createEncryptedOutputStream(credentials, kdbxHeader, encryptedOutputStream); - serializableDatabase.setHeaderHash(kdbxHeader.getHeaderHash()); - serializableDatabase.setEncryption(new Salsa20StreamEncryptor(kdbxHeader.getProtectedStreamKey())); - serializableDatabase.save(unencrytedOutputStream); - unencrytedOutputStream.flush(); - unencrytedOutputStream.close(); - } -} diff --git a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/Salsa20StreamEncryptor.java b/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/Salsa20StreamEncryptor.java deleted file mode 100644 index ce48264a..00000000 --- a/kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/Salsa20StreamEncryptor.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.pwdb.kdbx.stream_3_1; - -import org.linguafranca.pwdb.kdbx.StreamEncryptor; -import org.linguafranca.pwdb.security.Encryption; -import org.spongycastle.crypto.engines.Salsa20Engine; -import org.spongycastle.crypto.params.KeyParameter; -import org.spongycastle.crypto.params.ParametersWithIV; -import org.spongycastle.util.encoders.Hex; - -import java.security.MessageDigest; - -/** - * A helper class for Salsa20 encryption. - * - *

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

The encrypt and decrypt methods - * actually do the same thing. They are here - * only to fulfill the interface contract. - * - * @author jo - */ -public class Salsa20StreamEncryptor implements StreamEncryptor { - - private final Salsa20Engine salsa20; - private final byte[] key; - - // Android compatibility - private static final byte[] SALSA20_IV = Hex.decode("E830094B97205D2A".getBytes()); - - /** - * Creates a Salsa20 engine - * - * @param key the key to use - * @return an initialized Salsa20 engine - */ - @SuppressWarnings("WeakerAccess") - public static Salsa20Engine createSalsa20(byte[] key) { - MessageDigest md = Encryption.getMessageDigestInstance(); - KeyParameter keyParameter = new KeyParameter(md.digest(key)); - ParametersWithIV ivParameter = new ParametersWithIV(keyParameter, SALSA20_IV); - Salsa20Engine engine = new Salsa20Engine(); - engine.init(true, ivParameter); - return engine; - } - - /** - * Constructor creates engine used for both encryption and decryption - * - * @param key the key to use - */ - public Salsa20StreamEncryptor(byte[] key) { - this.key = key; - salsa20 = createSalsa20(key); - } - - @Override - public byte[] getKey() { - return key; - } - - @Override - public byte[] decrypt(byte[] encryptedText) { - byte[] output = new byte[encryptedText.length]; - salsa20.processBytes(encryptedText, 0, encryptedText.length, output, 0); - return output; - } - - @Override - public byte[] encrypt(byte[] decryptedText) { - byte[] output = new byte[decryptedText.length]; - salsa20.processBytes(decryptedText, 0, decryptedText.length, output, 0); - return output; - } -} diff --git a/kdbx/src/main/resources/base.kdbx.xml b/kdbx/src/main/resources/base.kdbx.xml index 5a791ae4..1958b946 100644 --- a/kdbx/src/main/resources/base.kdbx.xml +++ b/kdbx/src/main/resources/base.kdbx.xml @@ -16,7 +16,7 @@ - Keepass-Java-2 + KeePassJava2 New Database ${creationDate} diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/hashedblock/HashBlockTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/hashedblock/HashBlockTest.java index f0557357..8e16529f 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/hashedblock/HashBlockTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/hashedblock/HashBlockTest.java @@ -22,11 +22,13 @@ import java.util.Arrays; import static org.junit.Assert.*; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo */ public class HashBlockTest { + static PrintStream printStream = getTestPrintStream(); @Test public void testSmallBuf () throws IOException { @@ -108,7 +110,7 @@ private void createFile(File test, int length, byte pattern) throws IOException os.write(buf); os.flush(); os.close(); - System.out.println("File size is " + test.length() + " test buffer is " + length); + printStream.println("File size is " + test.length() + " test buffer is " + length); } } \ No newline at end of file diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelperTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelperTest.java index 113e2255..3a14a87f 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelperTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelperTest.java @@ -18,30 +18,37 @@ import org.junit.Test; +import java.io.PrintStream; import java.util.UUID; import static org.junit.Assert.assertEquals; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** - * @author jo + * tests the operation of Hex conversion */ public class HelperTest { + static PrintStream printStream = getTestPrintStream(); + @Test public void testBase64() throws Exception { String inputString = "0tU8XFRUX2TCk5tmmrshuQ=="; - System.out.println("Input string: " + inputString); + printStream.println("Input string: " + inputString); String hexString = "D2D53C5C54545F64C2939B669ABB21B9"; String convertedString = Helpers.hexStringFromBase64("0tU8XFRUX2TCk5tmmrshuQ=="); - System.out.println("Converted string: " + convertedString); + printStream.println("Converted string: " + convertedString); assertEquals(hexString, convertedString.toUpperCase()); + UUID uuid = Helpers.uuidFromBase64(inputString); - System.out.println("UUID: " + uuid.toString()); + printStream.println("UUID: " + uuid.toString()); assertEquals("d2d53c5c-5454-5f64-c293-9b669abb21b9", uuid.toString()); + String uuidHex = Helpers.hexStringFromUuid(uuid); - System.out.println("Hex from UUID: " + uuidHex); + printStream.println("Hex from UUID: " + uuidHex); assertEquals(hexString, uuidHex.toUpperCase()); + String base64 = Helpers.base64FromUuid(uuid); - System.out.println("Base 64: " + base64); + printStream.println("Base 64: " + base64); assertEquals(inputString, base64); } diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelpersTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelpersTest.java new file mode 100644 index 00000000..033cb2ab --- /dev/null +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/HelpersTest.java @@ -0,0 +1,67 @@ +package org.linguafranca.pwdb.kdbx; + +import org.junit.Test; + +import java.io.PrintStream; +import java.text.ParseException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Date; + +import static org.junit.Assert.*; +import static org.linguafranca.pwdb.kdbx.Helpers.dateTimeFormatter; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; + +public class HelpersTest { + + static PrintStream printStream = getTestPrintStream(); + + public String testDate = "2023-05-09T16:11:29Z"; + public Date testDateAsDate = Date.from(ZonedDateTime.parse(testDate, dateTimeFormatter).toInstant()); + public String v4Encoding = "sWfs2w4AAAA="; + + @Test + public void toDate() throws ParseException { + Date date = Helpers.toDate(v4Encoding); + assertEquals(testDateAsDate.getTime(), date.getTime()); + } + + @Test + public void toDate2() throws ParseException { + Date date = Helpers.toDate(testDate); + assertEquals(testDateAsDate, date); + } + + @Test + public void toDate3() throws ParseException { + ZonedDateTime zdt = ZonedDateTime.parse(testDate, dateTimeFormatter); + printStream.println(zdt); + Instant instant = zdt.toInstant(); + printStream.println(instant); + Date date = Date.from(instant); + printStream.println(date.clone()); + assertEquals(instant.toEpochMilli(), date.getTime()); + assertEquals(instant.toEpochMilli(), testDateAsDate.getTime()); + } + + @Test + public void fromDate() throws ParseException { + Helpers.isV4.set(false); + assertEquals(testDate, Helpers.fromDate(testDateAsDate)); + Helpers.isV4.set(true); + assertEquals(v4Encoding, Helpers.fromDate(testDateAsDate)); + } + + @Test + public void fromDateV3() throws ParseException { + assertEquals(testDate, Helpers.fromDateV3(testDateAsDate)); + } + + @Test + public void fromDateV4() throws ParseException { + String base64 = Helpers.fromDateV4(testDateAsDate); + printStream.println(Helpers.toDate(base64)); + assertEquals(v4Encoding, base64); + } +} \ No newline at end of file diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxHeaderTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxHeaderTest.java new file mode 100644 index 00000000..45946287 --- /dev/null +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxHeaderTest.java @@ -0,0 +1,69 @@ +package org.linguafranca.pwdb.kdbx; + +import com.google.common.io.CharStreams; +import com.google.common.io.LittleEndianDataInputStream; +import org.junit.Ignore; +import org.junit.Test; +import org.linguafranca.pwdb.hashedblock.HmacBlockInputStream; +import org.linguafranca.pwdb.kdbx.KdbxCreds; +import org.linguafranca.pwdb.kdbx.KdbxHeader; +import org.linguafranca.pwdb.kdbx.KdbxSerializer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.Assert.assertEquals; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; + +/** + * test decryption of various kinds + */ +public class KdbxHeaderTest { + static PrintStream printStream = getTestPrintStream(); + + @Test @Ignore // can be used for detailed HMAC debugging + public void getHmacStream() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); + KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); + printStream.println("Version " + header.getVersion()); + KdbxCreds creds = new KdbxCreds("123".getBytes()); + assert inputStream != null; + //noinspection UnstableApiUsage + KdbxSerializer.readOuterHeaderVerification(header, creds, new LittleEndianDataInputStream(inputStream)); + HmacBlockInputStream hmacBlockInputStream = new HmacBlockInputStream(header.getHmacKey(creds), inputStream, true); + printStream.println(CharStreams.toString(new InputStreamReader(hmacBlockInputStream, StandardCharsets.UTF_8))); + } + + // check the correct version + @Test + public void loadAesAesHeader() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-AES.kdbx"); + KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); + assertEquals(4, header.getVersion()); + } + + // check correct version + @Test + public void loadAesArgonHeader() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); + KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); + assertEquals(4, header.getVersion()); + } + + @Test + public void loadChaChaHeader() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha20-AES.kdbx"); + KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); + assertEquals(4, header.getVersion()); + } + @Test + public void loadChaChaArgonHeader() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha20-Argon2-Attachment.kdbx"); + KdbxHeader header = KdbxSerializer.readOuterHeader(inputStream, new KdbxHeader()); + assertEquals(4, header.getVersion()); + } + +} \ No newline at end of file diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java index c8c780e9..9bb6c352 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxKeyFileTest.java @@ -16,81 +16,79 @@ package org.linguafranca.pwdb.kdbx; +import com.google.common.io.CharStreams; import org.junit.Test; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxHeader; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxSerializer; import org.linguafranca.pwdb.Credentials; +import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** - * @author jo + * tests reading of kdbx with various combinations of key file and password */ public class KdbxKeyFileTest { + static PrintStream printStream = getTestPrintStream(); + + private static void toConsole(InputStream is) throws IOException { + printStream.println(CharStreams.toString(new InputStreamReader(is, StandardCharsets.UTF_8))); + } + /** + * Test that we can load a key file and get a 32 byte base64 encoded value back + */ @Test public void testLoad() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("KeyFileDatabase.key"); byte[] key = KdbxKeyFile.load(inputStream); assertNotNull(key); - assertEquals(32, key.length); + assertEquals(32, key.length);; } - /* - Test for empty password + /** + * Test that we can read a file with empty password */ @Test public void testEmptyPasswordCreds() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("EmptyPassword.kdbx"); Credentials credentials = new KdbxCreds(new byte[0]); InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); - byte[] buffer = new byte[1024]; - while ( decryptedInputStream.available() > 0) { - int read = decryptedInputStream.read(buffer); - if (read == -1) break; - System.out.write(buffer, 0, read); - } + toConsole(decryptedInputStream); } - /** - Test for empty password with key + Test for empty password with key file */ @Test public void testEmptyPasswordKeyCreds() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("EmptyPasswordWithKey.kdbx"); InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("EmptyPasswordWithKey.key"); + assert inputStreamKeyFile != null; Credentials credentials = new KdbxCreds(new byte[0], inputStreamKeyFile); InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); - byte[] buffer = new byte[1024]; - while ( decryptedInputStream.available() > 0) { - int read = decryptedInputStream.read(buffer); - if (read == -1) break; - System.out.write(buffer, 0, read); - } + toConsole(decryptedInputStream); } /** - Test for no master password + Test for no master password with key */ @Test public void testNoPasswordKeyCreds() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("NoPasswordWithKey.kdbx"); InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("NoPasswordWithKey.key"); + assert inputStreamKeyFile != null; Credentials credentials = new KdbxCreds(inputStreamKeyFile); InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); - byte[] buffer = new byte[1024]; - while ( decryptedInputStream.available() > 0) { - int read = decryptedInputStream.read(buffer); - if (read == -1) break; - System.out.write(buffer, 0, read); - } + toConsole(decryptedInputStream); } - /* + /** Test for empty password */ @Test @@ -98,11 +96,66 @@ public void testEmptyPassword() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("EmptyPassword.kdbx"); Credentials credentials = new KdbxCreds(new byte[0]); InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); - byte[] buffer = new byte[1024]; - while ( decryptedInputStream.available() > 0) { - int read = decryptedInputStream.read(buffer); - if (read == -1) break; - System.out.write(buffer, 0, read); - } + toConsole(decryptedInputStream); + } + + /** + * Test the hash in KeyFile (v2.0) + */ + @Test + public void testSignedKeyFile() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("kdbx_hash_test.kdbx"); + InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("kdbx_hash_test.keyx"); + Credentials credentials = new KdbxCreds("123".getBytes(), inputStreamKeyFile); + InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); + toConsole(decryptedInputStream); + } + + /** + * Test hash fails in KeyFile (v2.0) + */ + @Test(expected = RuntimeException.class) + public void testSignatureFails() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("kdbx_hash_test.kdbx"); + InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("kdbx_hash_test_wrong_hash.keyx"); + Credentials credentials = new KdbxCreds("123".getBytes(), inputStreamKeyFile); + InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); + toConsole(decryptedInputStream); + } + + /** + * Test KDBX with random KeyFile and key + */ + @Test + public void testKeyFileRandom() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("kdb_with_random_file.kdbx"); + InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("random_file"); + Credentials credentials = new KdbxCreds("123".getBytes(), inputStreamKeyFile); + InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); + toConsole(decryptedInputStream); + } + + /** + * Test KDBX with 64 bytes hex KeyFile and key + */ + @Test + public void testKeyFileHex64() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("kdbx_keyfile64.kdbx"); + InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("keyfile64"); + Credentials credentials = new KdbxCreds("123".getBytes(), inputStreamKeyFile); + InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); + toConsole(decryptedInputStream); + } + + /** + * Test KDBX with 32 bytes KeyFile and key + */ + @Test + public void testKeyFile32() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("kdbx_keyfile32.kdbx"); + InputStream inputStreamKeyFile = getClass().getClassLoader().getResourceAsStream("keyfile32"); + Credentials credentials = new KdbxCreds("123".getBytes(), inputStreamKeyFile); + InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); + toConsole(decryptedInputStream); } -} \ No newline at end of file +} diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java index 14ff37e2..14ccd4c2 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/kdbx/KdbxSerializerTest.java @@ -16,34 +16,51 @@ package org.linguafranca.pwdb.kdbx; +import com.google.common.io.CharStreams; import org.junit.Test; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxHeader; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxSerializer; import org.linguafranca.pwdb.Credentials; import java.io.*; +import java.nio.charset.StandardCharsets; import java.util.Scanner; import static org.junit.Assert.assertEquals; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** - * @author jo + * test operation of kdbx */ public class KdbxSerializerTest { + static PrintStream printStream = getTestPrintStream(); + + /** + * Test that we can read a kdbx v3 file and list the XML to console + */ @Test public void testGetPlainTextInputStream() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment.kdbx"); Credentials credentials = new KdbxCreds("123".getBytes()); InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); - byte[] buffer = new byte[1024]; - while ( decryptedInputStream.available() > 0) { - int read = decryptedInputStream.read(buffer); - if (read == -1) break; - System.out.write(buffer, 0, read); - } + printStream.println(CharStreams.toString(new InputStreamReader(decryptedInputStream, StandardCharsets.UTF_8))); + } + + /** + * Test that we can read a kdbx v4 file and list the XML to console + */ + @Test + public void testGetPlainTextInputStream2() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha20-AES.kdbx"); + Credentials credentials = new KdbxCreds("123".getBytes()); + InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); + printStream.println(CharStreams.toString(new InputStreamReader(decryptedInputStream, StandardCharsets.UTF_8))); } + + /** + * Test that we can write a KDBX 3 file containing "Hello World" + * and then read it back to get the same content + */ @Test public void testCypherTextOutputStream() throws Exception { File tempFile = File.createTempFile("test", "test"); @@ -59,4 +76,28 @@ public void testCypherTextOutputStream() throws Exception { Scanner scanner = new Scanner(inputStream); assertEquals("Hello World", scanner.nextLine()); } + + /** + * Test that we can write a KDBX 4 file containing "Hello World" + * and then read it back to get the same content + */ + @Test + public void testCypherTextOutputStream2() throws Exception { + File tempFile = File.createTempFile("test", "test"); + OutputStream testStream = new FileOutputStream(tempFile); + Credentials credentials = new KdbxCreds("123".getBytes()); + KdbxHeader kdbxHeader = new KdbxHeader(4); + OutputStream outputStream = KdbxSerializer.createEncryptedOutputStream(credentials, kdbxHeader, testStream); + + outputStream.write("Hello World\n".getBytes()); + outputStream.flush(); + outputStream.close(); + + KdbxHeader kdbxHeader1 = new KdbxHeader(); + try (InputStream inputStream = KdbxSerializer.createUnencryptedInputStream(credentials, kdbxHeader1, new FileInputStream(tempFile))){ + assertEquals(4, kdbxHeader1.getVersion()); + Scanner scanner = new Scanner(inputStream); + assertEquals("Hello World", scanner.nextLine()); + } + } } \ No newline at end of file diff --git a/kdbx/src/test/java/org/linguafranca/pwdb/security/Salsa20EncryptionTest.java b/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java similarity index 57% rename from kdbx/src/test/java/org/linguafranca/pwdb/security/Salsa20EncryptionTest.java rename to kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java index 5e504cf1..c3fbee0f 100644 --- a/kdbx/src/test/java/org/linguafranca/pwdb/security/Salsa20EncryptionTest.java +++ b/kdbx/src/test/java/org/linguafranca/pwdb/security/StreamEncryptionTest.java @@ -17,37 +17,54 @@ package org.linguafranca.pwdb.security; import org.junit.Test; -import org.linguafranca.pwdb.kdbx.stream_3_1.Salsa20StreamEncryptor; +import org.linguafranca.pwdb.security.StreamEncryptor.*; +import java.io.PrintStream; import java.security.SecureRandom; import static org.junit.Assert.assertEquals; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** + * Verify that the inner random encryption works two-way * @author jo */ -public class Salsa20EncryptionTest { +public class StreamEncryptionTest { + + static PrintStream printStream = getTestPrintStream(); + + @Test + public void salsa20() { + byte[] key = SecureRandom.getSeed(32); + StreamEncryptor ss = new Salsa20(key); + StreamEncryptor tt = new Salsa20(key); + + verifyTwoWay(ss, tt); + } @Test - public void encrypt() { + public void chacha20() { byte[] key = SecureRandom.getSeed(32); + StreamEncryptor ss = new ChaCha20(key); + StreamEncryptor tt = new ChaCha20(key); + + verifyTwoWay(ss, tt); + } + + private void verifyTwoWay(StreamEncryptor ss, StreamEncryptor tt) { - Salsa20StreamEncryptor ss = new Salsa20StreamEncryptor(key); byte[] e = ss.encrypt("new secret".getBytes()); byte[] f = ss.encrypt("secret 2".getBytes()); - Salsa20StreamEncryptor tt = new Salsa20StreamEncryptor(key); String s1 = new String(tt.encrypt(e)); String t1 = new String(tt.encrypt(f)); assertEquals("new secret", s1); assertEquals("secret 2", t1); - System.out.println(s1); - System.out.println(t1); - - + printStream.println(s1); + printStream.println(t1); } } \ No newline at end of file diff --git a/logback.xml b/logback.xml new file mode 100644 index 00000000..972c5a5f --- /dev/null +++ b/logback.xml @@ -0,0 +1,21 @@ + + + + + + -%highlight(%-5level) %d{HH:mm:ss.SSS} [%thread] %logger{36} - %cyan(%marker) %msg%n + + + + + + + + + 100 + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9f495a1d..2d42c209 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.linguafranca.pwdb KeePassJava2-parent - 2.1.1-SNAPSHOT + 2.2.4 database test @@ -29,19 +29,21 @@ dom simple jaxb + jackson all example + util pom - KeepassJava2 - A Java 7 API for databases compatible with the renowned KeePass password safe for Windows + KeePassJava2 + A Java 8 API for databases compatible with the renowned KeePass password safe for Windows https://github.com/jorabin/KeePassJava2 Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt + https://www.apache.org/licenses/LICENSE-2.0.txt repo @@ -50,8 +52,8 @@ scm:git:git://github.com/jorabin/KeePassJava2.git scm:git:git@github.com:jorabin/KeePassJava2.git https://github.com/jorabin/KeePassJava2 - HEAD - + KeePassJava2-2.2.4 + @@ -66,27 +68,61 @@ UTF-8 - 1.7 + 1.8 - 19.0 - 1.10 - 1.54.0.0 - 15.0 - 12.0 - 4.12 - 1.1.7 - 1.7.21 - - 3.3 - 2.19 - 1.6.7 - 2.4 - 2.10.3 - 1.6 - 2.5 + 3.11.0 + 3.0.0 + 1.6.13 + 3.2.1 + 3.5.0 + 3.0.1 + 3.0.0 + 3.0.2 + + false + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + false + + + ${inhibitConsoleOutput} + + + + ${maven.multiModuleProjectDirectory}/logback.xml + + + + + listener + org.linguafranca.test.maven.MavenRunListener + + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + ${project.groupId}.${project.artifactId} + + + + + + + org.apache.maven.plugins @@ -97,11 +133,6 @@ ${java.version} - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - org.apache.maven.plugins maven-release-plugin @@ -109,16 +140,91 @@ true false - releaseToMaven,signJar + releaseToMaven deploy + + org.apache.maven.plugins + maven-dependency-plugin + 3.8.1 + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + org.jetbrains + annotations + provided + + + junit + junit + test + + + + + + + org.jetbrains + annotations + 24.1.0 + + + + com.google.guava + guava + 32.1.2-android + + + org.bouncycastle + bcpkix-jdk18on + 1.78.1 + + + org.slf4j + slf4j-api + 2.0.16 + + + + ch.qos.logback + logback-classic + 1.3.15 + + + commons-codec + commons-codec + 1.15 + + + junit + junit + 4.13.2 + + + + - signJar + inhibitConsoleOutput + + true + + + + releaseToMaven @@ -135,13 +241,6 @@ - - - - - releaseToMaven - - org.sonatype.plugins nexus-staging-maven-plugin @@ -150,7 +249,7 @@ ossrh https://oss.sonatype.org/ - false + true @@ -176,6 +275,9 @@ jar + + html, syntax + @@ -191,4 +293,4 @@ - \ No newline at end of file + diff --git a/readme.md b/readme.md index 3b987702..2461f909 100644 --- a/readme.md +++ b/readme.md @@ -1,11 +1,22 @@ # KeePassJava2 -A Java 7 API for databases compatible with the renowned [KeePass](http://keepass.info) password -safe for Windows. +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.linguafranca.pwdb/KeePassJava2-parent) +[![javadoc](https://javadoc.io/badge2/org.linguafranca.pwdb/KeePassJava2/javadoc.svg)](https://javadoc.io/doc/org.linguafranca.pwdb/KeePassJava2) + +![alt text](https://badgen.net/badge/Build/2.2.4/blue?icon=github) +master [![CircleCI](https://dl.circleci.com/status-badge/img/gh/jorabin/KeePassJava2/tree/master.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/jorabin/KeePassJava2/tree/master) +develop [![CircleCI](https://dl.circleci.com/status-badge/img/gh/jorabin/KeePassJava2/tree/develop.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/jorabin/KeePassJava2/tree/develop) + + + +A Java 8 API for databases compatible with the renowned [KeePass](http://keepass.info) password +safe for Windows. This is a "headless" implementation - if you want something with a UI +then [KeePassXC](https://keepassxc.org/) and [KeePassDX](https://www.keepassdx.com/) could +be just the things for you. Features to date: -- Read and write KeePass 2.x format +- Read and write KeePass 2.x format (KDBX file formats V3 and V4) - Keepass 2.x Password and Keyfile Credentials - Read KeePass 1.x format (Rijndael only) - *No* requirement for JCE Policy Files @@ -26,65 +37,93 @@ It is licensed under the Apache 2 License and is currently usable. (see [license](#license)) -## Maven Coordinates +## Current Status + +The current code is version 2.2.4 - released to Maven March 2025. This is on the main branch. See [Build from Source](#build-from-source) + +Key updates relative to 2.1: +- Java 8 (dependencies no longer support Java 7) +- File format version 4 support - with Argon2 +- Inclusion of Jackson based KDBX support with a view to removing SimpleXML, JAXB and JAXB support +- Updated keyfile support +- Updated dependencies + +See the [changelog](CHANGELOG.md) for more details. + +## Maven Coordinates + +### Release + +The POM for the last release (2.2.4), Java 8 compatible, is + + org.linguafranca.pwdb + KeePassJava2-jackson + 2.2.4 -The composite POM is +at Maven Central. This provides access to the Jackson based implementation, +which is now the recommended implementation. There is also a composite POM that +provides access to all implementations (see [below](#database-implementations) for discussion). org.linguafranca.pwdb KeePassJava2 - 2.1.0 - - at Maven Central. Note that the artifactId has become Camel Case compared with release 2.0.1. + 2.2.4 + +Note that the artifactId has become Camel Case from release 2.1.x onwards. + +### Snapshot + +Snapshot builds are erratically available at [Sonatype](https://oss.sonatype.org/content/repositories/snapshots/org/linguafranca/pwdb/), next bug-fix release will be +2.2.5-SNAPSHOT (on branch `develop`) and work-in-progress 3.0.0-SNAPSHOT (on branch `v3`): + + org.linguafranca.pwdb + KeePassJava2 + 2.2.5-SNAPSHOT + +with appropriate `` entry, like: + + + + oss.sonatype.org-snapshot + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + There are also separate POMs for the various modules. The module structure is illustrated below - under [Build from Source](#bfs). - - Snapshot builds at [Sonatype OSS](https://oss.sonatype.org/content/groups/public/). + under [Build from Source](#build-from-source). ## Java Version -It is written for Java 1.7. +From release 2.2 it requires Java 1.8. From release 3, Java 11. Earlier versions require Java 1.7. ## Quick Start -Create credentials and an input stream for the password file in question: +Create credentials and an input stream for the password vault in question: KdbxCreds creds = new KdbxCreds("123".getBytes()); InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test1.kdbx"); -then choose a database implementation, and load the database. - - Database database = SimpleDatabase.load(credentials, inputStream) +then choose the Jackson based database implementation, and load the database -or + Database database = JacksonDatabase.load(creds, inputStream) - Database database = JaxbDatabase.load(credentials, inputStream) +See [below](#database-implementations) for discussion of other database implementations. Note that they will not be +maintained in the future - and see the following regarding making the storage of +passwords more secure. -or +### Storing Passwords - Database database = DomDatabaseWrapper.load(credentials, inputStream) - -Different implementations have varying characteristics, primarily speed. -The table below illustrates timings for the file `test1.kdbx` -(in the test module resources - -it is around 2k bytes and contains a few dozen entries) -as assessed by [this test](https://github.com/jorabin/KeePassJava2/blob/master/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java) - in the examples module. - - Simple 5 loads 20 iterations 257 millis - Jaxb 5 loads 20 iterations 326 millis - Dom 5 loads 20 iterations 758 millis +There are numerous well-understood problems +with storing passwords as Strings in Java. See [this discussion](./PropertyValueProtection.md) about the +KeePassJava2 approach to storing passwords. - Simple 10 loads 1 iterations 340 millis - Jaxb 10 loads 1 iterations 552 millis - Dom 10 loads 1 iterations 175 millis - - Simple 1 loads 50 iterations 28 millis - Jaxb 1 loads 50 iterations 47 millis - Dom 1 loads 50 iterations 251 millis - -Load time is dominant in this example for JAXB and Simple, -database traversal for the DOM implementation. +> Available from release 2.2.3 the enhanced **Jackson implementation** is the only KeePassJava2 database implementation that provides a +means of storing passwords other than as String. ### Discussion @@ -92,18 +131,18 @@ Password databases are modelled as a three layer abstraction. A *Database* is a collection of records whose physical representation needs only to be capable of rendering as a stream. *Entries* hold the information of value in the database and *Groups* allow the structuring of entries into collections, just like a folder structure. -The Database has a root group and by following sub-groups of the root group the tree structure of the database can be navigated. Entries belong to groups. Entries can be moved between groups and groups can also be moved between groups. However, entries and groups created in one database cannot be moved to another database without being converted: +The Database has a root group and by following subgroups of the root group the tree structure of the database can be navigated. Entries belong to groups. Entries can be moved between groups and groups can also be moved between groups. However, entries and groups created in one database cannot be moved to another database without being converted: database.newEntry(entryToCopy); database.newGroup(groupToCopy); The class Javadoc on Interface classes -[Database](database/src/main/java/org/linguafranca/pwdb/Database.java), -[Group](database/src/main/java/org/linguafranca/pwdb/Group.java) and -[Entry](database/src/main/java/org/linguafranca/pwdb/Entry.java) describe +[Database](http://javadoc.io/page/org.linguafranca.pwdb/database/latest/org/linguafranca/pwdb/Database.html), +[Group](http://javadoc.io/page/org.linguafranca.pwdb/database/latest/org/linguafranca/pwdb/Group.html) and +[Entry](http://javadoc.io/page/org.linguafranca.pwdb/database/latest/org/linguafranca/pwdb/Entry.html) describe how to use the methods of those classes to create and modify entries. These classes provide the basis of all implementations of the various database formats, -initially KDB and KDBX 3.1 (KeePass 2) file formats, subsequently, potentially, others. +initially KDB, KDBX 3.1 and KDBX 4 (KeePass 2) file formats, subsequently, potentially, others. The class [QuickStart.java](example/src/main/java/org/linguafranca/pwdb/kdbx/QuickStart.java) provides some illustrations of operations using the Database, Group and Entry interfaces. @@ -114,73 +153,155 @@ This project is so named by kind permission of Dominik Reichl the author of KeeP is no formal connection with that project. It has always been the intention to support other specific password database implementations. -Hence the creation of abstract Database interfaces rather than following the KeePass model +Hence, the creation of abstract Database interfaces rather than following the KeePass model exactly. -KeePass is in effect defined by the code that Dominik writes to create and maintain the project. -Hence there are no definitive specification of KeePass files other than that code. For the sake of -clarification and my own satisfaction I have written about my understanding of KeePass formats in the following locations: +KeePass is in effect defined by the code that Dominik writes to create and maintain the project and +[KDBX File Format Specification](https://keepass.info/help/kb/kdbx.html) describes the file format. There +is also a discussion of the [differences between KDBX version 3.1 and version 4](https://keepass.info/help/kb/kdbx_4.html). +Additionally, there is a discussion of the [enhancements in KDBX 4.1](https://keepass.info/help/kb/kdbx_4.1.html), as well +as a discussion of [Key Files](https://keepass.info/help/base/keys.html#keyfiles). + +Massive credit also to the folks over at [KeePassXC](https://keepassxc.org/) who wrote some +[documentation](https://github.com/keepassxreboot/keepassxc-specs) about their understanding of various format things. Also, this is a +useful [discussion/investigation](https://github.com/scubajorgen/KeepassDecrypt) of the KDBX format. + +For the sake of +clarification and my own satisfaction I have written about my understanding of +KeePass formats in the following locations: + +1. The Javadoc header to [KdbxSerializer](http://javadoc.io/page/org.linguafranca.pwdb/KeePassJava2-kdbx/latest/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.html) describes KDBX stream formatting. +2. The XSD Schema [KDBX.4.xsd](KDBX.4.xsd) documents my understanding of the Keepass XML, and also my + lack of understanding, in parts. While preparing release 2.2.3 I found [this XSD](https://keepass.info/help/download/KDBX_XML.xsd) at the + KeePass site. I have not (so far) attempted to reconcile my documentation with it. +3. The following graphic illustrates KDBX 3.1 and 4 file formats: + + +[![KDBX Formats](KdbxDiagram.svg "KDBX Formats")](KdbxDiagram.svg) + +## Database Implementations + +KeePass - or more specifically its file format KDBX - is an XML based format, so one of the main tasks +is serializing and deserializing XML. Over time (KeePassJava2 was originally released in 2014) approaches +to Java and XML have been a bit mysterious. However, Jackson has now been chosen as the +underlying framework for implementation of KeePassJava2. + +There are several other database implementations which will be maintained for bug-fix purposes +only, with a view to being withdrawn, since they perform badly and/or depend on obsolete technology. + +- `SimpleXML` - no longer maintained, does not work with Java 17 and up +- `JAXB` - this causes problems with `javax` and `jakarta` namespaces, it's not worth maintaining + as it offers no compelling performance or other advantage +- `DOM` the was the original implementation and validates the fact that DOM based implementations + are slow. That said, if you want to load a database and then save it while maintaining whatever quirks + existed in the original database then this is the one. -1. The Javadoc header to [KdbxSerializer](kdbx/src/main/java/org/linguafranca/pwdb/kdbx/stream_3_1/KdbxSerializer.java) describes KDBX stream formatting. -2. The XSD Schema [KDBX.3.1.xsd](KDBX.3.1.xsd) documents my understanding of the Keepass XML, and also my lack of understanding, in parts. -3. [This graphic](Format Diagram.svg) illustrates KDBX 3.1 stream format and also illustrates proposals for the revised KDBX 4.0 format. +Aside from dependencies on underlying frameworks, different implementations have varying characteristics, primarily speed. This is assessed +by [this test](https://github.com/jorabin/KeePassJava2/blob/master/example/src/main/java/org/linguafranca/pwdb/kdbx/OpenDbExample.java) in the module `examples`. ## Dependencies -Aside from the JRE the API depends on: +Aside from the JRE, at release 2.2, the API depends on: - [Google Guava](https://github.com/google/guava/wiki) ([Apache 2 license](https://github.com/google/guava/blob/master/COPYING)). - [Apache Commons Codec](https://commons.apache.org/proper/commons-codec/) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0)). -- [Spongy Castle](https://rtyley.github.io/spongycastle/) which is a repackaging for Android of [Bouncy Castle](https://www.bouncycastle.org/java.html) ([Apache 2 license](https://www.bouncycastle.org/licence.html)). +- [Bouncy Castle](https://github.com/bcgit/bc-java/blob/master/LICENSE.html) ([MIT License](https://github.com/bcgit/bc-java/blob/master/LICENSE.html)). -The Simple XML implementation additionally depends on: +The Jackson implementation depends on: + +- [Faster XML Jackson](https://github.com/FasterXML/jackson) + +The (historical) Simple XML implementation additionally depends on: - [Simple XML Serialisation Framework](http://simple.sourceforge.net/) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0)). - [Faster XML Aalto](https://github.com/FasterXML/aalto-xml) ([Apache 2 license](http://www.apache.org/licenses/LICENSE-2.0.txt)). -It also depends on SLF4J and Junit for tests. +For Java 11 and later, Jaxb implementation depends on explicit inclusion [no longer provided by JDK](https://docs.oracle.com/en/java/javase/11/migrate/index.html#JSMIG-GUID-F640FA9D-FB66-4D85-AD2B-D931174C09A3) of: + +- [JAXB](https://javaee.github.io/jaxb-v2/) + +It also depends on SLF4J and Junit 4 for tests. ## Build from Source Included POM is for Maven 3. -### Module Structure +### Module Structure There are rather a lot of modules, this is in order to allow loading of minimal necessary functionality. The module dependencies are illustrated below. -![Module Structure](Module Structure.svg) +[![Module Structure](ModuleStructure.svg "Module Structure")](./ModuleStructure.svg) -Each module corresponds to a Maven artifact. The GroupId is `org.linguafranca.pwdb`. The version id is as noted [above](#mvn). +Each module corresponds to a Maven artifact. The GroupId is `org.linguafranca.pwdb`. The version id is as noted [above](#maven-coordinates). - + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + +
ModuleArtifactIdDescription
ModuleArtifactIdJavaDocDescription
databasedatabaseBase definition of the Database APIs.
exampleexampleWorked examples of loading, saving, splicing etc. using the APIs
testtestShared tests to assess the viability of the implementation.
allKeePassJava2This is the main KeePassJava2 Maven dependency. Provides a route to all artifacts (other than test and examples) via transitive dependency.
kdbKeePassJava2-kdbAn implementation of the Database APIs supporting KeePass KDB format.
kdbxKeePassJava2-kdbxProvides support for KDBX streaming and security.
simpleKeePassJava2-simpleA Simple XML Platform implementation of KDBX. Could be useful for Android.
jaxbKeePassJava2-jaxbA JAXB implementation of KDBX. Probably not useful for Android. The generated class + +
databasedatabase +Javadocs +Base definition of the Database APIs.
exampleexampleJavadocsWorked examples of loading, saving, splicing etc. using the APIs
testtestJavadocsShared tests to assess the viability of the implementation.
allKeePassJava2(no JavaDoc)This is the main KeePassJava2 Maven dependency. Provides a route to all artifacts (other than test and examples) via transitive dependency.
kdbKeePassJava2-kdbJavadocsAn implementation of the Database APIs supporting KeePass KDB format.
kdbxKeePassJava2-kdbxJavadocsProvides support for KDBX streaming and security.
jacksonKeePassJava2-jacksonJavadocsA Jackson based implementation of KDBX. Intended to provide the main support going forward, replacing trhe +modules below.
simpleKeePassJava2-simpleJavadocsA Simple XML Platform implementation of KDBX. Could be useful for Android. +Simple cannot be used with Java versions 17 and up.
jaxbKeePassJava2-jaxbJavadocsA JAXB implementation of KDBX. Probably not useful for Android. The generated class bindings might be useful for building other interfaces.
domKeePassJava2-domA DOM based implementation of KDBX. Being DOM based it is rather slow, but -messes less with existing content than the other two implementations. Known to work on Android.
domKeePassJava2-domJavadocsA DOM based implementation of KDBX. Being DOM based it is rather slow, but +messes less with existing content than the other implementations. Known to work on Android.
-Why are there so many implementations for KDBX? Well, the DOM implementation came first, because of the fact that it can +Why are there so many implementations for KDBX? Well, the DOM implementation came first, because it can load and save stuff that the implementation doesn't specifically know about. But it is very slow. Then came the JAXB implementation, but -belatedly it seems that Android support is in question. So latterly the Simple implementation. That's probably enough -KDBX implementations. +belatedly it seems that Android support is in question. So latterly the Simple implementation. That was probably enough +KDBX implementations, however, the Simple XML library seems no longer to be maintained, +and along comes the Jackson Implementation. + +Jackson is the implementation going forward. ### Gradle -If you prefer Gradle the automatic conversion `gradle init` converts the POM successfully, however you will +If you prefer Gradle the automatic conversion `gradle init` has been known to convert the POM successfully, however you will need to add something like [gradle-source-sets.txt](jaxb/gradle-source-sets.txt) to the `build.gradle` for the JAXB module, so that the generated sources get compiled correctly. @@ -191,11 +312,16 @@ In [this file](./CHANGELOG.md). ## Acknowledgements Many thanks to Pavel Ivanov [@ivanovpv](https://github.com/ivanovpv) for -his help with Android and Gradle compatibility issues. +his help with Android and Gradle compatibility issues back in the very early days. + +Thanks to Giuseppe Valente [@giusvale-dev](https://github.com/giusvale-dev) for +his contribution of the Jackson module and enhancements to KeyFile support. + +Thanks to other contributors and raisers of issues. -## License +## License -Copyright (c) 2016 Jo Rabin +Copyright (c) 2025 Jo Rabin Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/simple/pom.xml b/simple/pom.xml index a7d3615a..4e06e62d 100644 --- a/simple/pom.xml +++ b/simple/pom.xml @@ -19,7 +19,7 @@ KeePassJava2-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.2.4 ../pom.xml 4.0.0 @@ -38,30 +38,23 @@ org.linguafranca.pwdb test ${project.version} + test-jar test - junit - junit - ${junit.version} - test - - - org.simpleframework - simple-xml + com.carrotsearch.thirdparty + simple-xml-safe 2.7.1 org.apache.httpcomponents httpcore - 4.4.5 + 4.4.16 com.fasterxml aalto-xml - 1.0.0 + 1.3.3 - - \ No newline at end of file diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxInputTransformer.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxInputTransformer.java deleted file mode 100644 index 5f208a14..00000000 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxInputTransformer.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.pwdb.kdbx; - -import org.linguafranca.xml.XmlEventTransformer; - -import javax.xml.namespace.QName; -import javax.xml.stream.XMLEventFactory; -import javax.xml.stream.events.Attribute; -import javax.xml.stream.events.XMLEvent; - -import static javax.xml.stream.XMLStreamConstants.CHARACTERS; -import static javax.xml.stream.XMLStreamConstants.END_ELEMENT; -import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; - -/** - * @author jo - */ -@SuppressWarnings("WeakerAccess") -public class KdbxInputTransformer implements XmlEventTransformer { - private XMLEventFactory xmlEventFactory = new com.fasterxml.aalto.stax.EventFactoryImpl(); - private final StreamEncryptor streamEncryptor; - private boolean decryptContent; - - public KdbxInputTransformer (StreamEncryptor streamEncryptor) { - this.streamEncryptor = streamEncryptor; - } - - public XMLEvent transform (XMLEvent event) { - switch (event.getEventType()) { - case START_ELEMENT: { - Attribute attribute = event.asStartElement().getAttributeByName(new QName("Protected")); - if (attribute != null) { - decryptContent = Helpers.toBoolean(attribute.getValue()); - } - break; - } - case END_ELEMENT: { - decryptContent = false; - break; - } - case CHARACTERS: { - if (decryptContent) { - String text = event.asCharacters().getData(); - text = new String(streamEncryptor.decrypt(Helpers.decodeBase64Content(text.getBytes(), false))); - event = xmlEventFactory.createCharacters(text); - } - break; - } - } - return event; - } -} diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxOutputTransformer.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxOutputTransformer.java deleted file mode 100644 index 9013d26c..00000000 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/KdbxOutputTransformer.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.pwdb.kdbx; - -import org.linguafranca.xml.XmlEventTransformer; - -import javax.xml.namespace.QName; -import javax.xml.stream.XMLEventFactory; -import javax.xml.stream.events.Attribute; -import javax.xml.stream.events.XMLEvent; - -import static javax.xml.stream.XMLStreamConstants.CHARACTERS; -import static javax.xml.stream.XMLStreamConstants.END_ELEMENT; -import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; - -/** - * @author jo - */ -@SuppressWarnings("WeakerAccess") -public class KdbxOutputTransformer implements XmlEventTransformer { - - private XMLEventFactory eventFactory = com.fasterxml.aalto.stax.EventFactoryImpl.newInstance(); - private StreamEncryptor encryptor; - private Boolean encryptContent = false; - - public KdbxOutputTransformer(StreamEncryptor encryptor) { - this.encryptor = encryptor; - } - - @Override - public XMLEvent transform(XMLEvent event) { - switch (event.getEventType()) { - case START_ELEMENT: { - Attribute attribute = event.asStartElement().getAttributeByName(new QName("Protected")); - if (attribute != null) { - encryptContent = Helpers.toBoolean(attribute.getValue()); - } - break; - } - case CHARACTERS: { - if (encryptContent) { - String unencrypted = event.asCharacters().getData(); - String encrypted = Helpers.encodeBase64Content(encryptor.encrypt(unencrypted.getBytes()), false); - event = eventFactory.createCharacters(encrypted); - } - break; - } - case END_ELEMENT: { - encryptContent = false; - break; - } - } - return event; - } -} - diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java index 3f879655..7cafa52a 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabase.java @@ -16,29 +16,26 @@ package org.linguafranca.pwdb.kdbx.simple; +import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.StreamConfiguration; +import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.base.AbstractDatabase; -import org.linguafranca.pwdb.kdbx.KdbxInputTransformer; -import org.linguafranca.pwdb.kdbx.KdbxOutputTransformer; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxHeader; -import org.linguafranca.pwdb.kdbx.stream_3_1.KdbxSerializer; -import org.linguafranca.pwdb.kdbx.stream_3_1.Salsa20StreamEncryptor; -import org.linguafranca.pwdb.kdbx.simple.converter.*; -import org.linguafranca.pwdb.kdbx.simple.model.EntryClasses; +import org.linguafranca.pwdb.kdbx.KdbxHeader; +import org.linguafranca.pwdb.kdbx.KdbxStreamFormat; import org.linguafranca.pwdb.kdbx.simple.model.KeePassFile; -import org.linguafranca.pwdb.Credentials; -import org.linguafranca.xml.XmlInputStreamFilter; -import org.linguafranca.xml.XmlOutputStreamFilter; -import org.simpleframework.xml.*; -import org.simpleframework.xml.convert.AnnotationStrategy; -import org.simpleframework.xml.convert.Registry; -import org.simpleframework.xml.convert.RegistryStrategy; -import org.simpleframework.xml.core.Persister; -import org.simpleframework.xml.strategy.Strategy; +import org.linguafranca.pwdb.security.Encryption; +import org.linguafranca.pwdb.security.StreamEncryptor; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.*; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import static org.linguafranca.pwdb.kdbx.simple.SimpleSerializableDatabase.createEmptyDatabase; +import static org.linguafranca.pwdb.kdbx.simple.SimpleSerializableDatabase.getSerializer; /** * Implementation of {@link org.linguafranca.pwdb.Database} using the Simple XML framework. @@ -48,30 +45,83 @@ @SuppressWarnings("WeakerAccess") public class SimpleDatabase extends AbstractDatabase{ - private KeePassFile keePassFile; + KeePassFile keePassFile; + StreamFormat streamFormat; /** * Create a new empty database */ public SimpleDatabase() { + this(createEmptyDatabase(), null); + } + + public SimpleDatabase(KeePassFile file, StreamFormat streamFormat) { try { - keePassFile = createEmptyDatabase(); + keePassFile = file; keePassFile.root.group.database = this; + this.streamFormat = streamFormat; + SimpleSerializableDatabase.fixUp(keePassFile.root.group); } catch (Exception e) { throw new IllegalStateException(e); } } + /** + * Load plaintext XML + * + * @param inputStream contains the XML + * @return a new Database + * @throws Exception on load failure + */ + public static SimpleDatabase loadXml(InputStream inputStream) throws Exception { + KeePassFile result = getSerializer(new StreamEncryptor.None()).read(KeePassFile.class, inputStream); + result.root.group.uuid = UUID.randomUUID(); + return new SimpleDatabase(result, null); + } + + /** + * Load kdbx file + * + * @param credentials credentials to use + * @param inputStream where to load from + * @return a new database + */ + public static SimpleDatabase load(Credentials credentials, InputStream inputStream) throws IOException { + SimpleSerializableDatabase simpleSerializableDatabase = new SimpleSerializableDatabase(); + StreamFormat streamFormat = new KdbxStreamFormat(); + streamFormat.load(simpleSerializableDatabase, credentials, inputStream); + return new SimpleDatabase(simpleSerializableDatabase.getKeePassFile(), streamFormat); + } + + /** + * Save the database with the same stream format that it was loaded with, or V4 default if none + * @param credentials credentials to use + * @param outputStream where to write to + */ + @Override + public void save(Credentials credentials, OutputStream outputStream) throws IOException { + // save with the stream format used to load if it exists, otherwise save V4 + if (Objects.isNull(streamFormat)) { + streamFormat = new KdbxStreamFormat(new KdbxHeader(4)); + } + save(streamFormat, credentials, outputStream); + } /** - * Create a database instance from a keepass file - * @param keePassFile the instance to initialise from + * 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 */ - protected SimpleDatabase (KeePassFile keePassFile) { - this.keePassFile = keePassFile; - this.keePassFile.root.group.database = this; - fixUp(this.keePassFile.root.group); + @Override + public void save(StreamFormat streamFormat, Credentials credentials, + OutputStream outputStream) throws IOException{ + keePassFile.meta.generator = "KeePassJava2-Simple"; + SimpleSerializableDatabase simpleSerializableDatabase = new SimpleSerializableDatabase(this.keePassFile); + streamFormat.save(simpleSerializableDatabase, credentials, outputStream); + setDirty(false); } + @Override public SimpleGroup getRootGroup() { return keePassFile.root.getGroup(); @@ -99,6 +149,29 @@ public SimpleIcon newIcon(Integer integer) { return ic; } + @Override + public boolean isRecycleBinEnabled() { + return this.keePassFile.meta.recycleBinEnabled; + } + + @Override + public void enableRecycleBin(boolean enable) { + this.keePassFile.meta.recycleBinEnabled = enable; + } + + @Override + public SimpleGroup getRecycleBin() { + UUID recycleBinUuid = this.keePassFile.meta.recycleBinUUID; + SimpleGroup g = findGroup(recycleBinUuid); + if (g == null && isRecycleBinEnabled()) { + g = newGroup("Recycle Bin"); + getRootGroup().addGroup(g); + this.keePassFile.meta.recycleBinUUID = g.getUuid(); + this.keePassFile.meta.recycleBinChanged = new Date(); + } + return g; + } + @Override public String getName() { return keePassFile.meta.databaseName; @@ -123,157 +196,21 @@ public void setDescription(String s) { setDirty(true); } - /** - * Create an empty underlying KeePassFile instance - * - * @return a new database - * @throws Exception on failure - */ - private static KeePassFile createEmptyDatabase() throws Exception { - InputStream inputStream = SimpleDatabase.class.getClassLoader().getResourceAsStream("base.kdbx.xml"); - return getSerializer().read(KeePassFile.class, inputStream); - } - - /** - * Load plaintext XML - * - * @param inputStream contains the XML - * @return a new Database - * @throws Exception on load failure - */ - public static SimpleDatabase loadXml(InputStream inputStream) throws Exception { - KeePassFile result = getSerializer().read(KeePassFile.class, inputStream); - result.root.group.uuid = UUID.randomUUID(); - return new SimpleDatabase(result); - } - - /** - * Load kdbx file - * - * @param credentials the credentials to use - * @param inputStream the encrypted input stream - * @return a new database - * @throws Exception on load failure - */ - public static SimpleDatabase load(Credentials credentials, InputStream inputStream) throws Exception { - - // load the KDBX header and get the inner Kdbx stream - KdbxHeader kdbxHeader = new KdbxHeader(); - InputStream kdbxInnerStream = KdbxSerializer.createUnencryptedInputStream(credentials, kdbxHeader, inputStream); - - // decrypt the encrypted fields in the inner XML stream - InputStream plainTextXmlStream = new XmlInputStreamFilter(kdbxInnerStream, - new KdbxInputTransformer(new Salsa20StreamEncryptor(kdbxHeader.getProtectedStreamKey()))); - - // read the now entirely decrypted stream into database - KeePassFile result = getSerializer().read(KeePassFile.class, plainTextXmlStream); - if (!Arrays.equals(result.meta.headerHash.getContent(), kdbxHeader.getHeaderHash())) { - throw new IllegalStateException("Header Hash Mismatch"); - } - - return new SimpleDatabase(result); - } - - /** - * Save as plaintext XML - * - * @param outputStream the destination to save to - */ - public void save(OutputStream outputStream) { - try { - prepareForSave(keePassFile.root.group); - - // and save the database out - getSerializer().write(this.keePassFile, outputStream); - - } catch (Exception e) { - throw new IllegalStateException(e); - } - - } - - @Override - public void save(Credentials credentials, OutputStream outputStream) throws IOException { - try { - // create the stream to accept unencrypted data and output to encrypted - KdbxHeader kdbxHeader = new KdbxHeader(); - OutputStream kdbxInnerStream = KdbxSerializer.createEncryptedOutputStream(credentials, kdbxHeader, outputStream); - - // the database contains the hash of the headers - keePassFile.meta.headerHash.setContent(kdbxHeader.getHeaderHash()); - - // encrypt the fields in the XML inner stream - XmlOutputStreamFilter plainTextOutputStream = new XmlOutputStreamFilter(kdbxInnerStream, - new KdbxOutputTransformer(new Salsa20StreamEncryptor(kdbxHeader.getProtectedStreamKey()))); - - // set up the "protected" attributes of fields that need inner stream encryption - prepareForSave(keePassFile.root.group); - - // and save the database out - getSerializer().write(this.keePassFile, plainTextOutputStream); - plainTextOutputStream.close(); - plainTextOutputStream.await(); - this.setDirty(false); - - } catch (Exception e) { - throw new IllegalStateException(e); - } - } - @Override public boolean shouldProtect(String s) { return keePassFile.meta.memoryProtection.shouldProtect(s); } - public List getBinaries() { + public List getBinaries() { return keePassFile.getBinaries(); } - /** - * Utility to get a simple framework persister - * @return a persister - * @throws Exception when things get tough - */ - private static Serializer getSerializer() throws Exception { - Registry registry = new Registry(); - registry.bind(String.class, EmptyStringConverter.class); - Strategy strategy = new AnnotationStrategy(new RegistryStrategy(registry)); - return new Persister(strategy); - + public void addBinary(byte [] bytes, int index) { + SimpleSerializableDatabase.addBinary(this.keePassFile, index, bytes); } - /** - * Utility to add in back links to parent group and database - * - * @param parent the group to start from - */ - private static void fixUp(SimpleGroup parent){ - for (SimpleGroup group: parent.group) { - group.parent = parent; - group.database = parent.database; - fixUp(group); - } - for (SimpleEntry entry: parent.entry) { - entry.database = parent.database; - entry.parent = parent; - } - } - - /** - * Utility to mark fields that need to be encrypted and vice versa - * - * @param parent the group to start from - */ - private static void prepareForSave(SimpleGroup parent){ - for (SimpleGroup group: parent.group) { - prepareForSave(group); - } - for (SimpleEntry entry: parent.entry) { - for (EntryClasses.StringProperty property : entry.string) { - boolean shouldProtect = parent.database.shouldProtect(property.getKey()); - property.getValue().setProtected(shouldProtect); - } - } + public StreamFormat getStreamFormat() { + return streamFormat; } } \ No newline at end of file diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java index 366a8845..93f4f105 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntry.java @@ -16,6 +16,7 @@ package org.linguafranca.pwdb.kdbx.simple; +import org.jetbrains.annotations.NotNull; import org.linguafranca.pwdb.base.AbstractEntry; import org.linguafranca.pwdb.kdbx.Helpers; import org.linguafranca.pwdb.kdbx.simple.converter.UuidConverter; @@ -40,7 +41,7 @@ * * @author jo */ -@SuppressWarnings({"WeakerAccess"}) +@SuppressWarnings({"WeakerAccess", "unused"}) @Root(name="Entry") public class SimpleEntry extends AbstractEntry { @Element(name = "UUID", type=UUID.class) @@ -78,7 +79,7 @@ public class SimpleEntry extends AbstractEntry(); binary = new ArrayList<>(); - times = new Times(new Date()); + times = new Times(); uuid = UUID.randomUUID(); iconID = 0; } @@ -92,8 +93,9 @@ public static SimpleEntry createEntry(SimpleDatabase database) { SimpleEntry result = new SimpleEntry(); result.database = database; result.parent = null; + // avoiding setProperty as it does a touch(); for (String p: STANDARD_PROPERTY_NAMES) { - result.setProperty(p, ""); + result.string.add(new EntryClasses.StringProperty(p, new EntryClasses.StringProperty.Value(""))); } return result; } @@ -113,6 +115,20 @@ public void setProperty(String s, String s1) { touch(); } + @Override + public boolean removeProperty(String name) throws IllegalArgumentException { + if (STANDARD_PROPERTY_NAMES.contains(name)) throw new IllegalArgumentException("may not remove property: " + name); + + EntryClasses.StringProperty sp = getStringProperty(name, string); + if (sp == null) { + return false; + } else { + this.string.remove(sp); + touch(); + return true; + } + } + @Override public List getPropertyNames() { List result = new ArrayList<>(); @@ -129,8 +145,8 @@ public byte[] getBinaryProperty(String s) { return null; } - KeePassFile.Binaries.Binary binary = null; - for (KeePassFile.Binaries.Binary b : database.getBinaries()) { + KeePassFile.Binary binary = null; + for (KeePassFile.Binary b : database.getBinaries()) { if (b.getId().equals(Integer.valueOf(getBinaryContent(bp)))) { binary = b; } @@ -151,19 +167,14 @@ public void setBinaryProperty(String s, byte[] bytes) { // what is the next free index in the binary store? Integer max = -1; - for (KeePassFile.Binaries.Binary binary: database.getBinaries()){ + for (KeePassFile.Binary binary: database.getBinaries()){ if (binary.getId() > max) { max = binary.getId(); } } max++; - // create a new binary to put in the store - KeePassFile.Binaries.Binary newBin = new KeePassFile.Binaries.Binary(); - newBin.setId(max); - newBin.setValue(Helpers.encodeBase64Content(bytes, true)); - newBin.setCompressed(true); - database.getBinaries().add(newBin); + database.addBinary(bytes, max); // make a reference to it from the entry BinaryProperty binaryProperty = new BinaryProperty(); @@ -175,6 +186,17 @@ public void setBinaryProperty(String s, byte[] bytes) { touch(); } + @Override + public boolean removeBinaryProperty(String name) throws UnsupportedOperationException { + BinaryProperty bp = getBinaryProp(name, binary); + if (bp != null) { + binary.remove(bp); + touch(); + return true; + } + return false; + } + @Override public List getBinaryPropertyNames() { List result = new ArrayList<>(); @@ -190,7 +212,7 @@ public SimpleGroup getParent() { } @Override - public UUID getUuid() { + public @NotNull UUID getUuid() { return uuid; } @@ -219,11 +241,22 @@ public boolean getExpires() { return times.getExpires(); } + @Override + public void setExpires(boolean expires) { + times.setExpires(expires); + } + @Override public Date getExpiryTime() { return times.getExpiryTime(); } + @Override + public void setExpiryTime(Date expiryTime) throws IllegalArgumentException { + if (expiryTime == null) throw new IllegalArgumentException("expiryTime may not be null"); + times.setExpiryTime(expiryTime); + } + @Override public Date getLastModificationTime() { return times.getLastModificationTime(); diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java index 131de74e..5fb36258 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/SimpleGroup.java @@ -16,6 +16,7 @@ package org.linguafranca.pwdb.kdbx.simple; +import org.jetbrains.annotations.NotNull; import org.linguafranca.pwdb.Group; import org.linguafranca.pwdb.kdbx.simple.converter.KeePassBooleanConverter; import org.linguafranca.pwdb.kdbx.simple.converter.UuidConverter; @@ -35,7 +36,7 @@ * Implementation of {@link Group} using the Simple XML framework. * @author jo */ -@SuppressWarnings("WeakerAccess") +@SuppressWarnings({"WeakerAccess", "unused"}) @Root(name = "Group") public class SimpleGroup extends org.linguafranca.pwdb.base.AbstractGroup { @Element(name = "UUID", type = UUID.class) @@ -47,6 +48,9 @@ public class SimpleGroup extends org.linguafranca.pwdb.base.AbstractGroup { + + private final StreamEncryptor encryption; + + public ValueConverter(StreamEncryptor encryption) { + this.encryption = encryption; + } + + /** + * De-serialise (unmarshal) the input node + * @param node this is the node to deserialize the object from + * + * @return the de-serialised object + */ + @Override + public EntryClasses.StringProperty.Value read(InputNode node) throws Exception { + EntryClasses.StringProperty.Value value = new EntryClasses.StringProperty.Value(); + if (Objects.nonNull(node.getAttribute("Protected")) && node.getAttribute("Protected").getValue().equals("True")) { + byte[] encrypted = new byte[0]; + // you can only read the value once + String text = node.getValue(); + if (Objects.nonNull(text)) { + encrypted = Base64.decodeBase64(text.getBytes()); + } + String decrypted = new String(encryption.decrypt(encrypted), StandardCharsets.UTF_8); + value.setText(decrypted); + value.setProtected(null); + value.setProtectOnOutput(true); + } else { + value.setProtectOnOutput(false); + value.setText(node.getValue()); + } + return value; + } + + /** + * Serialise (marshal) the object + * @param node this is the node to serialized the object to + * @param value this is the value that is to be serialized + */ + @Override + public void write(OutputNode node, EntryClasses.StringProperty.Value value) throws Exception { + if (value.getProtectOnOutput()) { + byte [] encrypted = encryption.encrypt(value.getText().getBytes(StandardCharsets.UTF_8)); + byte [] base64Encoded = Base64.encodeBase64(encrypted); + node.setValue(new String(base64Encoded)); + node.getAttributes().put("Protected", "True"); + } else { + node.setValue(value.getText()); + } + //node.getAttributes().remove("protectInMemory"); + } +} diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/EntryClasses.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/EntryClasses.java index cb4fd553..02285fe5 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/EntryClasses.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/EntryClasses.java @@ -16,20 +16,19 @@ package org.linguafranca.pwdb.kdbx.simple.model; -import org.linguafranca.pwdb.base.AbstractEntry; import org.linguafranca.pwdb.kdbx.simple.SimpleEntry; import org.linguafranca.pwdb.kdbx.simple.converter.KeePassBooleanConverter; -import org.simpleframework.xml.Attribute; -import org.simpleframework.xml.Element; -import org.simpleframework.xml.Root; -import org.simpleframework.xml.Text; +import org.simpleframework.xml.*; import org.simpleframework.xml.convert.Convert; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * @author jo */ +@SuppressWarnings("unused") public abstract class EntryClasses { public static StringProperty getStringProperty(String name, List string) { for (StringProperty property : string) { @@ -74,7 +73,6 @@ public static class Association { protected String window; @Element(name="KeystrokeSequence") protected String keystrokeSequence; - @SuppressWarnings("unused") protected List windowAndKeystrokeSequence; } } @@ -91,10 +89,10 @@ public StringProperty(String key, Value value) { } @Element(name="Key") - protected String key; + String key; @Element(name="Value") - protected Value value; + Value value; @Override public String getName() { @@ -128,13 +126,36 @@ public Value(String text, Boolean _protected) { @Convert(KeePassBooleanConverter.class) protected Boolean protectInMemory; @Attribute(name = "Protected", required = false) + Boolean _protected; + @Attribute(name = "kpj2-ProtectOnOutput", required = false) @Convert(KeePassBooleanConverter.class) - protected Boolean _protected; + Boolean protectOnOutput; @Text - protected String text; + String text; - public void setProtected(boolean aProtected) { - this._protected = aProtected; + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public void setProtectOnOutput(boolean aProtected) { + this.protectOnOutput = aProtected; + } + + public boolean getProtectOnOutput() { + return Objects.nonNull(this.protectOnOutput) && this.protectOnOutput; + } + + + public Boolean getProtected() { + return _protected; + } + + public void setProtected(Boolean _protected) { + this._protected = _protected; } } } @@ -143,10 +164,10 @@ public void setProtected(boolean aProtected) { public static class BinaryProperty implements org.simpleframework.xml.util.Entry { @Element(name="Key") - protected String key; + String key; @Element(name="Value") - protected Value value; + Value value; public String getKey() { return key; @@ -168,7 +189,7 @@ public void setValue(Value value) { @Root(name="Value") public static class Value { @Attribute(name="Ref") - protected String ref; + String ref; public void setRef(String ref) { this.ref = ref; @@ -176,9 +197,16 @@ public void setRef(String ref) { } } - @Root(name = "History") public static class History { - @Element(name = "SimpleEntry", required = false) - protected SimpleEntry entry; + + @ElementList(entry = "SimpleEntry", inline = true) + private List list; + + public History(){ + list = new ArrayList<>(); + } + public List getHistory(){ + return list; + } } } diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java index c2a33c13..3883eca1 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/KeePassFile.java @@ -17,7 +17,6 @@ package org.linguafranca.pwdb.kdbx.simple.model; import org.linguafranca.pwdb.Entry; -import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; import org.linguafranca.pwdb.kdbx.simple.SimpleGroup; import org.linguafranca.pwdb.kdbx.simple.converter.Base64ByteArrayConverter; import org.linguafranca.pwdb.kdbx.simple.converter.KeePassBooleanConverter; @@ -25,7 +24,6 @@ import org.linguafranca.pwdb.kdbx.simple.converter.UuidConverter; import org.simpleframework.xml.*; import org.simpleframework.xml.convert.Convert; -import org.simpleframework.xml.util.Dictionary; import java.util.ArrayList; import java.util.Date; @@ -44,10 +42,14 @@ public class KeePassFile { @Element(name = "Root") public Root root; - public List getBinaries() { + public List getBinaries() { return meta.binaries; } + public void createBinaries() { + meta.binaries = new ArrayList<>(); + } + public static class Root { @Element(name = "Group") public SimpleGroup group; @@ -62,7 +64,7 @@ public SimpleGroup getGroup() { @SuppressWarnings("unused") public static class Meta { @Element(name = "Generator") - protected String generator; + public String generator; @Element(name = "HeaderHash", required = false) @Convert(Base64ByteArrayConverter.class) public KeePassFile.ByteArray headerHash; @@ -98,13 +100,13 @@ public static class Meta { protected ArrayList customIcons; @Element(name = "RecycleBinEnabled", type = Boolean.class) @Convert(KeePassBooleanConverter.class) - protected Boolean recycleBinEnabled; + public Boolean recycleBinEnabled; @Element(name = "RecycleBinUUID", type = UUID.class) @Convert(UuidConverter.class) - protected UUID recycleBinUUID; + public UUID recycleBinUUID; @Element(name = "RecycleBinChanged", type = Date.class) @Convert(TimeConverter.class) - protected Date recycleBinChanged; + public Date recycleBinChanged; @Element(name = "EntryTemplatesGroup", type = UUID.class) @Convert(UuidConverter.class) protected UUID entryTemplatesGroup; @@ -122,9 +124,15 @@ public static class Meta { @Element(name = "HistoryMaxSize") protected int historyMaxSize; @ElementList(name = "Binaries", required = false) - protected List binaries; + protected List binaries; @Element(name = "CustomData", required = false) protected KeePassFile.CustomData customData; + + /* version 4 */ + + @Element(name = "SettingsChanged", required = false, type = Date.class) + @Convert(TimeConverter.class) + protected Date settingsChanged; } @@ -158,48 +166,47 @@ public Boolean shouldProtect(String name) { } public static class Binaries { - @ElementList(name = "Binary") - protected List binary; - - @org.simpleframework.xml.Root(name = "Binary") - public static class Binary implements org.simpleframework.xml.util.Entry { - @Text - protected String value; - - @Attribute(name = "ID") - protected Integer id; - @Attribute(name = "Compressed") - @Convert(KeePassBooleanConverter.class) - protected Boolean compressed; - - @Override - public String getName() { - return String.valueOf(id); - } - public String getValue() { - return value; - } + } - public Boolean getCompressed() { - return compressed; - } + @org.simpleframework.xml.Root(name = "Binary") + public static class Binary implements org.simpleframework.xml.util.Entry { + @Text + protected String value; - public Integer getId() { - return id; - } + @Attribute(name = "ID") + protected Integer id; + @Attribute(name = "Compressed") + @Convert(KeePassBooleanConverter.class) + protected Boolean compressed; - public void setId(Integer Id) { - this.id = Id; - } + @Override + public String getName() { + return String.valueOf(id); + } - public void setValue(String value) { - this.value = value; - } + public String getValue() { + return value; + } - public void setCompressed(boolean compressed) { - this.compressed = compressed; - } + public Boolean getCompressed() { + return compressed; + } + + public Integer getId() { + return id; + } + + public void setId(Integer Id) { + this.id = Id; + } + + public void setValue(String value) { + this.value = value; + } + + public void setCompressed(boolean compressed) { + this.compressed = compressed; } } @@ -208,6 +215,11 @@ public static class Icon implements org.simpleframework.xml.util.Entry { @Element(name = "UUID", type=UUID.class) @Convert(UuidConverter.class) protected UUID uuid; + @Element(name = "Name") + protected String name; + @Element(name = "LastModificationTime", type=Date.class) + @Convert(TimeConverter.class) + protected Date lastModificationTime; @Element(name = "Data") @Convert(Base64ByteArrayConverter.class) protected ByteArray data; @@ -237,6 +249,7 @@ public void setContent(byte[] content) { } } + @org.simpleframework.xml.Root(strict=false) public static class CustomData { protected List any; } diff --git a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/Times.java b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/Times.java index 20a8aa59..b96388e4 100644 --- a/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/Times.java +++ b/simple/src/main/java/org/linguafranca/pwdb/kdbx/simple/model/Times.java @@ -27,26 +27,27 @@ /** * @author jo */ +@SuppressWarnings({"unused", "WeakerAccess"}) @Root public class Times { - @Element(name = "LastModificationTime", required = true, type = Date.class) + @Element(name = "LastModificationTime", type = Date.class) @Convert(TimeConverter.class) protected Date lastModificationTime; - @Element(name = "CreationTime", required = true, type = Date.class) + @Element(name = "CreationTime", type = Date.class) @Convert(TimeConverter.class) protected Date creationTime; - @Element(name = "LastAccessTime", required = true, type = Date.class) + @Element(name = "LastAccessTime", type = Date.class) @Convert(TimeConverter.class) protected Date lastAccessTime; - @Element(name = "ExpiryTime", required = true, type = Date.class) + @Element(name = "ExpiryTime", type = Date.class) @Convert(TimeConverter.class) protected Date expiryTime; - @Element(name = "Expires", required = true, type = Boolean.class) + @Element(name = "Expires", type = Boolean.class) @Convert(KeePassBooleanConverter.class) protected Boolean expires; @Element(name = "UsageCount") protected int usageCount; - @Element(name = "LocationChanged", required = true, type = Date.class) + @Element(name = "LocationChanged", type = Date.class) @Convert(TimeConverter.class) protected Date locationChanged; @@ -107,7 +108,7 @@ public void setLocationChanged(Date locationChanged) { } public Times() { - this(new Date()); + this(new Date(System.currentTimeMillis()/1000*1000)); } public Times(Date date) { lastModificationTime = date; diff --git a/simple/src/main/java/org/linguafranca/xml/XmlInputStreamFilter.java b/simple/src/main/java/org/linguafranca/xml/XmlInputStreamFilter.java deleted file mode 100644 index a951f5cc..00000000 --- a/simple/src/main/java/org/linguafranca/xml/XmlInputStreamFilter.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.xml; - -import org.jetbrains.annotations.NotNull; - -import javax.xml.stream.*; -import javax.xml.stream.events.XMLEvent; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; - - -/** - * A filter to accept a stream, interpret as XML, allow transformation - * as XML then forward as a stream. - * - *

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

Although this means that the interpretation of the XML will happen - * twice, here and in the target application, some such applications - * do not accept XML streams. e.g. the Simple XML framework. - * - * @author jo - */ -@SuppressWarnings({"WeakerAccess", "unused"}) -public class XmlOutputStreamFilter extends PipedOutputStream { - - Future future; - - public XmlOutputStreamFilter(final OutputStream outputStream, final XmlEventTransformer eventTransformer) throws IOException { - - Callable output = new Callable() { - public Boolean call() { - try { - XMLEventReader eventReader = new com.fasterxml.aalto.stax.InputFactoryImpl() - .createXMLEventReader(new PipedInputStream(XmlOutputStreamFilter.this)); - XMLEventWriter eventWriter = new com.fasterxml.aalto.stax.OutputFactoryImpl() - .createXMLEventWriter(outputStream); - - XMLEvent event = null; - while (eventReader.hasNext()) { - event = eventReader.nextEvent(); - event = eventTransformer.transform(event); - eventWriter.add(event); - eventWriter.flush(); - } - - eventReader.close(); - eventWriter.flush(); - eventWriter.close(); - outputStream.flush(); - outputStream.close(); - } catch (XMLStreamException | IOException e) { - throw new IllegalStateException(e); - } - return true; - } - }; - future = Executors.newSingleThreadExecutor().submit(output); - } - - public void cancel(boolean interrupt){ - future.cancel(interrupt); - } - - public boolean isDone(){ - return future.isDone(); - } - - public boolean await() throws IOException { - try { - return future.get(); - } catch (InterruptedException | ExecutionException e) { - throw new IOException(e); - } - } - - @Override - public void flush() throws IOException { - super.flush(); - } - - @Override - public void close() throws IOException { - super.close(); - } -} - diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java deleted file mode 100644 index 307c3ce2..00000000 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerInputStreamTest.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.pwdb.kdbx; - -import org.junit.Test; -import org.linguafranca.xml.XmlInputStreamFilter; - -import javax.xml.stream.XMLStreamException; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; - -/** - * @author jo - */ -public class KdbxInnerInputStreamTest { - - - @Test - public void test() throws XMLStreamException, IOException { - InputStream is = getClass().getClassLoader().getResourceAsStream("ExampleDatabase.xml"); - XmlInputStreamFilter sxd = new XmlInputStreamFilter(is, new KdbxInputTransformer(new org.linguafranca.pwdb.kdbx.stream_3_1.Salsa20StreamEncryptor.None())); - int len; - do { - byte b[] = new byte[8096]; - len = sxd.read(b); - if (len >-1) - System.out.print(new String(b, 0, len, Charset.forName("UTF-8"))); - } while (len > -1); - } - -} \ No newline at end of file diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerOutputStreamTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerOutputStreamTest.java deleted file mode 100644 index e16def67..00000000 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/KdbxInnerOutputStreamTest.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.pwdb.kdbx; - -import com.google.common.io.ByteStreams; -import org.junit.Ignore; -import org.junit.Test; -import org.linguafranca.pwdb.Database; -import org.linguafranca.pwdb.Entry; -import org.linguafranca.pwdb.kdbx.simple.SimpleDatabase; -import org.linguafranca.pwdb.kdbx.simple.SimpleEntry; -import org.linguafranca.pwdb.Credentials; -import org.linguafranca.xml.XmlEventTransformer; -import org.linguafranca.xml.XmlInputStreamFilter; -import org.linguafranca.xml.XmlOutputStreamFilter; - -import javax.xml.stream.XMLStreamException; -import java.io.*; -import java.security.SecureRandom; - -import static org.junit.Assert.assertEquals; - -/** - * @author jo - */ -public class KdbxInnerOutputStreamTest { - - @Test - public void test() throws Exception { - final SimpleDatabase database = new SimpleDatabase(); - final SimpleEntry entry = database.newEntry(); - entry.setTitle("Password Encyption Test"); - entry.setPassword("password"); - database.getRootGroup().addEntry(entry); - database.save(new KdbxCreds.None(), ByteStreams.nullOutputStream()); - } - - @Test @Ignore - public void testOutputStreamFilter () throws IOException, XMLStreamException { - File temp = File.createTempFile("temp", "temp"); - OutputStream outputStream = new FileOutputStream(temp); - XmlOutputStreamFilter filter = new XmlOutputStreamFilter(outputStream, new KdbxOutputTransformer.None()); - outputStream.write("hello world".getBytes()); - outputStream.flush(); - outputStream.close(); - - InputStream inputStream = new FileInputStream(temp); - XmlInputStreamFilter filter1 = new XmlInputStreamFilter(inputStream, new XmlEventTransformer.None()); - byte[] b = new byte[1024]; - int l = filter1.read(b); - String s = new String(b,0,l); - assertEquals("hello world", s); - } - - -} \ No newline at end of file diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntryTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV3Test.java similarity index 67% rename from simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntryTest.java rename to simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV3Test.java index 96b7f384..be8bde9b 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleEntryTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV3Test.java @@ -16,7 +16,7 @@ package org.linguafranca.pwdb.kdbx.simple; -import org.linguafranca.pwdb.checks.EntryChecks; +import org.linguafranca.pwdb.checks.BinaryPropertyChecks; import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.kdbx.KdbxCreds; import org.linguafranca.pwdb.Credentials; @@ -28,9 +28,9 @@ /** * @author jo */ -public class SimpleEntryTest extends EntryChecks { +public class SimpleBinaryPropertyV3Test extends BinaryPropertyChecks { - public SimpleEntryTest() { + public SimpleBinaryPropertyV3Test() { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("Attachment.kdbx"); try { database = SimpleDatabase.load(new KdbxCreds("123".getBytes()),inputStream); @@ -43,4 +43,25 @@ public SimpleEntryTest() { public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { database.save(credentials, outputStream); } + + + @Override + public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + try { + return SimpleDatabase.load(credentials, inputStream); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + @Override + public Database newDatabase() { + return new SimpleDatabase(); + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } } diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java new file mode 100644 index 00000000..aca2335d --- /dev/null +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleBinaryPropertyV4Test.java @@ -0,0 +1,64 @@ +/* + * Copyright 2015 Jo Rabin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.kdbx.simple; + +import org.junit.Ignore; +import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.checks.BinaryPropertyChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * @author jo + */ +@Ignore +public class SimpleBinaryPropertyV4Test extends BinaryPropertyChecks { + + public SimpleBinaryPropertyV4Test() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-ChaCha20-Argon2-Attachment.kdbx"); + database = SimpleDatabase.load(new KdbxCreds("123".getBytes()), inputStream); + } + + @Override + public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { + database.save(credentials, outputStream); + } + + @Override + public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + try { + return SimpleDatabase.load(credentials, inputStream); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public Database newDatabase() { + return new SimpleDatabase(); + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } + +} \ No newline at end of file diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java index 01e22d02..d6250be4 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleDatabaseLoadTest.java @@ -16,34 +16,61 @@ package org.linguafranca.pwdb.kdbx.simple; +import org.junit.Ignore; import org.junit.Test; +import org.linguafranca.pwdb.Entry; import org.linguafranca.pwdb.Visitor; import org.linguafranca.pwdb.kdbx.KdbxCreds; import java.io.InputStream; +import java.io.PrintStream; + +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo */ public class SimpleDatabaseLoadTest { - @Test + static PrintStream printStream = getTestPrintStream(); + + @Test @Ignore(value = "Looks like this example is bogus") public void loadXml() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("ExampleDatabase.xml"); SimpleDatabase database = SimpleDatabase.loadXml(inputStream); - database.visit(new Visitor.Print()); + database.visit(new Visitor.Print(printStream)); } @Test public void loadKdbx() throws Exception { InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test123.kdbx"); SimpleDatabase database = SimpleDatabase.load(new KdbxCreds("123".getBytes()), inputStream); - database.visit(new Visitor.Print()); + database.visit(new Visitor.Print(printStream)); + } + @Test + public void loadKdbxV4() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("V4-AES-Argon2.kdbx"); + SimpleDatabase database = SimpleDatabase.load(new KdbxCreds("123".getBytes()), inputStream); + database.visit(new Visitor.Print(printStream)); + // test what happens to dates in V4 + database.visit(new Visitor.Default(){ + @Override + public void visit(Entry entry) { + printStream.println(entry.getCreationTime()); + } + }); } @Test public void emptyDb() throws Exception { SimpleDatabase database = new SimpleDatabase(); - System.out.println(database.getDescription()); + printStream.println(database.getDescription()); + } + + @Test + public void dbWithDeleted() throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("testDeleted.kdbx"); + SimpleDatabase database = SimpleDatabase.load(new KdbxCreds("123".getBytes()), inputStream); + database.visit(new Visitor.Print(printStream)); } } \ No newline at end of file diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimplePropertyValueTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimplePropertyValueTest.java new file mode 100644 index 00000000..bd81a9bc --- /dev/null +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimplePropertyValueTest.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.simple; + +import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.checks.PropertyValueChecks; +import org.linguafranca.pwdb.kdbx.KdbxCreds; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class SimplePropertyValueTest extends PropertyValueChecks { + + public SimplePropertyValueTest() throws IOException { + super(false); + } + + + @Override + public Database createDatabase() throws IOException { + return new SimpleDatabase(); + } + + @Override + public void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException { + database.save(credentials, outputStream); + } + + @Override + public Database loadDatabase(Credentials credentials, InputStream inputStream) throws IOException { + try { + return SimpleDatabase.load(credentials, inputStream); + } catch (Exception e) { + throw new IOException(e); + } + } + + @Override + public Credentials getCreds(byte[] creds) { + return new KdbxCreds(creds); + } +} diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleRecycleBinTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleRecycleBinTest.java new file mode 100644 index 00000000..3d7a4895 --- /dev/null +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleRecycleBinTest.java @@ -0,0 +1,12 @@ +package org.linguafranca.pwdb.kdbx.simple; + +import org.linguafranca.pwdb.checks.RecycleBinChecks; + +/** + * @author jo + */ +public class SimpleRecycleBinTest extends RecycleBinChecks { + public SimpleRecycleBinTest() { + database = new SimpleDatabase(); + } +} diff --git a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java index a035fc9a..ab394211 100644 --- a/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java +++ b/simple/src/test/java/org/linguafranca/pwdb/kdbx/simple/SimpleSaveAndReloadTest.java @@ -16,20 +16,27 @@ package org.linguafranca.pwdb.kdbx.simple; +import org.junit.Test; +import org.linguafranca.pwdb.StreamFormat; import org.linguafranca.pwdb.checks.SaveAndReloadChecks; import org.linguafranca.pwdb.Database; import org.linguafranca.pwdb.kdbx.KdbxCreds; -import org.linguafranca.pwdb.kdbx.StreamFormat; import org.linguafranca.pwdb.Credentials; +import org.linguafranca.pwdb.kdbx.KdbxHeader; +import org.linguafranca.pwdb.kdbx.KdbxSerializer; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.*; + +import static org.junit.Assert.assertTrue; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo */ public class SimpleSaveAndReloadTest extends SaveAndReloadChecks { + + static PrintStream printStream = getTestPrintStream(); + @Override public Database getDatabase() { return new SimpleDatabase(); @@ -59,6 +66,45 @@ public Database loadDatabase(Credentials credentials, InputStream inputStream) t } } + + @Override + public boolean verifyStreamFormat(StreamFormat s1, StreamFormat s2) { + KdbxHeader h1 = (KdbxHeader) s1.getStreamConfiguration(); + KdbxHeader h2 = (KdbxHeader) s1.getStreamConfiguration(); + return (h1.getVersion() == h2.getVersion() && + h1.getProtectedStreamAlgorithm().equals(h2.getProtectedStreamAlgorithm()) && + h1.getKeyDerivationFunction().equals(h2.getKeyDerivationFunction()) && + h1.getCipherAlgorithm().equals(h2.getCipherAlgorithm())); + } + + + // check that boolean comes out in upper case - Simple Converters don't work on attributes + // so this is done in the output transformer + @Test + public void uppercaseBooleanTest() throws IOException { + SimpleDatabase s = new SimpleDatabase(); + SimpleEntry e = s.newEntry(); + e.setPassword("12345"); + s.getRootGroup().addEntry(e); + File file = File.createTempFile("kdbx", "kdbx"); + s.save(new KdbxCreds("123".getBytes()), new FileOutputStream(file)); + InputStream inputStream = new FileInputStream(file); + Credentials credentials = new KdbxCreds("123".getBytes()); + InputStream decryptedInputStream = KdbxSerializer.createUnencryptedInputStream(credentials, new KdbxHeader(), inputStream); + BufferedReader br = new BufferedReader(new InputStreamReader(decryptedInputStream)); + boolean foundValue = false; + while (br.ready()) { + String string = br.readLine(); + if (string.trim().startsWith(" KeePassJava2-parent org.linguafranca.pwdb - 2.1.1-SNAPSHOT + 2.2.4 4.0.0 @@ -11,6 +11,10 @@ PWDB :: Test Contains the shared test classes for exercising the functionality of a PWDB database implementation. + + true + + org.linguafranca.pwdb @@ -18,25 +22,27 @@ ${project.version} - junit - junit - ${junit.version} - - - org.slf4j - slf4j-api - ${slf4j-api.version} - - - ch.qos.logback - logback-core - ${logback.version} - - - ch.qos.logback - logback-classic - ${logback.version} + org.linguafranca.pwdb + util + 2.2.4 + test + + + + org.apache.maven.plugins + maven-jar-plugin + 2.2 + + + + test-jar + + + + + + \ No newline at end of file diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/EntryChecks.java b/test/src/main/java/org/linguafranca/pwdb/checks/EntryChecks.java deleted file mode 100644 index cbd20806..00000000 --- a/test/src/main/java/org/linguafranca/pwdb/checks/EntryChecks.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2015 Jo Rabin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.linguafranca.pwdb.checks; - -import com.google.common.io.ByteStreams; -import org.junit.Assert; - -import org.junit.Test; -import org.linguafranca.pwdb.Credentials; -import org.linguafranca.pwdb.Database; -import org.linguafranca.pwdb.Entry; - - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import static org.junit.Assert.assertArrayEquals; - - -/** - * @author jo - */ -public abstract class EntryChecks { - public Database database; - @SuppressWarnings("unused") - public abstract void saveDatabase(Database database, Credentials credentials, OutputStream outputStream) throws IOException; - - @Test - public void getBinaryProperty() throws Exception { - Entry entry = database.findEntries("Test attachment").get(0); - byte [] letterJ = entry.getBinaryProperty("letter J.jpeg"); - InputStream testfile = getClass().getClassLoader().getResourceAsStream("letter J.jpeg"); - byte [] original = ByteStreams.toByteArray(testfile); - Assert.assertArrayEquals(original, letterJ); - } - - @Test - public void getAnotherBinaryProperty() throws Exception { - Entry entry = database.findEntries("Test 2 attachment").get(0); - byte [] letterL = entry.getBinaryProperty("letter L.jpeg"); - InputStream testfile = getClass().getClassLoader().getResourceAsStream("letter L.jpeg"); - byte [] original = ByteStreams.toByteArray(testfile); - Assert.assertArrayEquals(original, letterL); - } - - @Test - public void setBinaryProperty() throws Exception { - InputStream testfile = getClass().getClassLoader().getResourceAsStream("letter L.jpeg"); - byte [] original = ByteStreams.toByteArray(testfile); - Entry entry = database.findEntries("Test attachment").get(0); - entry.setBinaryProperty("letter L.jpeg", original); - byte [] letterL = entry.getBinaryProperty("letter L.jpeg"); - Assert.assertArrayEquals(original, letterL); - assertArrayEquals(new String[] {"letter J.jpeg", "letter L.jpeg"}, entry.getBinaryPropertyNames().toArray()); - Assert.assertTrue(database.isDirty()); - } - - @Test - public void getBinaryPropertyNames() throws Exception { - Entry entry = database.findEntries("Test attachment").get(0); - assertArrayEquals(new String[] {"letter J.jpeg"}, entry.getBinaryPropertyNames().toArray()); - - entry = database.findEntries("Test 2 attachment").get(0); - assertArrayEquals(new String[] {"letter J.jpeg", "letter L.jpeg"}, entry.getBinaryPropertyNames().toArray()); - } -} diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java b/test/src/test/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java similarity index 51% rename from test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java rename to test/src/test/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java index 9c7fb462..dd0d06b5 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java +++ b/test/src/test/java/org/linguafranca/pwdb/checks/BasicDatabaseChecks.java @@ -24,7 +24,9 @@ import org.linguafranca.pwdb.Icon; import java.io.IOException; +import java.time.Instant; import java.util.ArrayList; +import java.util.Date; import java.util.List; import static org.junit.Assert.*; @@ -32,11 +34,11 @@ /** * @author Jo */ -public abstract class BasicDatabaseChecks { +public abstract class BasicDatabaseChecks , G extends Group, E extends Entry, I extends Icon> { - private Database database; + protected Database database; - public abstract Database createDatabase() throws IOException; + public abstract Database createDatabase() throws IOException; public BasicDatabaseChecks() throws IOException { this.database = createDatabase(); @@ -44,21 +46,21 @@ public BasicDatabaseChecks() throws IOException { @Test public void testEmptyDatabase() { - Assert.assertTrue (database.getRootGroup().getName().equals("Root")); - Assert.assertTrue (database.getRootGroup().getEntries().size() == 0); - Assert.assertTrue (database.getRootGroup().getGroups().size() == 0); + assertEquals("Root", database.getRootGroup().getName()); + assertEquals(0, database.getRootGroup().getEntries().size()); + assertEquals(0, database.getRootGroup().getGroups().size()); } @Test public void testAddGroup() { - Group g1 = database.getRootGroup().addGroup(database.newGroup("group1")); - Assert.assertTrue (database.getRootGroup().getGroups().size() == 1); - Assert.assertTrue (g1.getName().equals("group1")); - Assert.assertTrue (g1.getGroups().size() == 0); - Assert.assertTrue (g1.getEntries().size() == 0); - Assert.assertTrue("root is not the parent of its child", g1.getParent().equals(database.getRootGroup())); - - Group g2 = database.newGroup(); + G g1 = database.getRootGroup().addGroup(database.newGroup("group1")); + assertEquals(1, database.getRootGroup().getGroups().size()); + assertEquals("group1", g1.getName()); + assertEquals(0, g1.getGroups().size()); + assertEquals(0, g1.getEntries().size()); + assertEquals("root is not the parent of its child", g1.getParent(), database.getRootGroup()); + + G g2 = database.newGroup(); assertEquals("", g2.getName()); Assert.assertNotNull(g2.getUuid()); assertEquals(0, g2.getIcon().getIndex()); @@ -67,70 +69,70 @@ public void testAddGroup() { // show that the list of groups is a copy database.getRootGroup().getGroups().clear(); - Assert.assertTrue(database.getRootGroup().getGroups().size() == 1); - Assert.assertTrue (g1.getEntries().size() == 0); + assertEquals(1, database.getRootGroup().getGroups().size()); + assertEquals(0, g1.getEntries().size()); } @Test public void testDeleteGroup () { - Group g1 = database.getRootGroup().addGroup(database.newGroup("group1")); - List l1 = database.getRootGroup().findGroups("group1"); - Assert.assertTrue(l1.size() == 1); - Group g2 = l1.get(0); - Assert.assertTrue (g2.equals(g1)); - Group g3 = database.getRootGroup().removeGroup(g2); - Assert.assertTrue (g3.equals(g1)); - Assert.assertTrue(g1.getParent() == null); - Assert.assertTrue(database.getRootGroup().getGroups().size() == 0); - Assert.assertTrue(database.getRootGroup().findGroups("group1").size() == 0); + Group g1 = database.getRootGroup().addGroup(database.newGroup("group1")); + List l1 = database.getRootGroup().findGroups("group1"); + assertEquals(1, l1.size()); + G g2 = l1.get(0); + assertEquals(g2, g1); + G g3 = database.getRootGroup().removeGroup(g2); + assertEquals(g3, g1); + assertNull(g1.getParent()); + assertEquals(0, database.getRootGroup().getGroups().size()); + assertEquals(0, database.getRootGroup().findGroups("group1").size()); } @Test public void testAddRemoveEntry() { - Entry e1 = database.getRootGroup().addEntry(database.newEntry()); + E e1 = database.getRootGroup().addEntry(database.newEntry()); e1.setTitle("entry1"); - List l1 = database.findEntries("entry1"); - Assert.assertTrue(l1.size() == 1); + List l1 = database.findEntries("entry1"); + assertEquals(1, l1.size()); - Entry e12 = database.getRootGroup().addEntry(database.newEntry("entry12")); - List l2 = database.findEntries("entry1"); - Assert.assertTrue(l2.size() == 2); + E e12 = database.getRootGroup().addEntry(database.newEntry("entry12")); + List l2 = database.findEntries("entry1"); + assertEquals(2, l2.size()); // show that the entries are different - Assert.assertFalse(l2.get(0).equals(l2.get(1))); + assertNotEquals(l2.get(0), l2.get(1)); // show that the list is a copy l2.clear(); - Assert.assertTrue(database.findEntries("entry1").size() == 2); + assertEquals(2, database.findEntries("entry1").size()); // show that we get an equivalent entry when we remove to when we inserted - Entry e12b = database.getRootGroup().removeEntry(e12); - Assert.assertTrue(e12b.equals(e12)); + E e12b = database.getRootGroup().removeEntry(e12); + assertEquals(e12b, e12); // has been unhooked from parent - Assert.assertTrue(e12.getParent() == null); - Assert.assertTrue(database.findEntries("entry1").size() == 1); + assertNull(e12.getParent()); + assertEquals(1, database.findEntries("entry1").size()); } @Test public void testSetFields () { - Entry e1 = database.newEntry("Entry 1"); + E e1 = database.newEntry("Entry 1"); e1.setNotes("this looks a little like Entry 2"); - Assert.assertTrue(e1.getNotes().equals("this looks a little like Entry 2")); + assertEquals("this looks a little like Entry 2", e1.getNotes()); e1.setUsername("jake@window.com"); - Assert.assertTrue(e1.getUsername().equals("jake@window.com")); + assertEquals("jake@window.com", e1.getUsername()); e1.setPassword("supercalifragelisticexpialidocious"); - Assert.assertTrue(e1.getPassword().equals("supercalifragelisticexpialidocious")); - e1.setUrl("http://window.com"); - Assert.assertTrue(e1.getUrl().equals("http://window.com")); + assertEquals("supercalifragelisticexpialidocious", e1.getPassword()); + e1.setUrl("https://window.com"); + assertEquals("https://window.com", e1.getUrl()); Assert.assertTrue(e1.match("2")); Assert.assertTrue(e1.matchTitle("1")); Assert.assertFalse(e1.matchTitle("doggy")); - Icon ic1 = database.newIcon(27); + I ic1 = database.newIcon(27); e1.setIcon(ic1); - Assert.assertTrue(e1.getIcon().equals(ic1)); + assertEquals(e1.getIcon(), ic1); // databases have to support setting of standard properties e1.setProperty(Entry.STANDARD_PROPERTY_NAME_TITLE, "A title"); @@ -154,6 +156,7 @@ public void testSetFields () { Assert.assertEquals(0, properties.size()); } catch (UnsupportedOperationException e) { // databases don't have to support arbitrary properties + assertFalse(database.supportsNonStandardPropertyNames()); assertArrayEquals(e1.getPropertyNames().toArray(), Entry.STANDARD_PROPERTY_NAMES.toArray()); } @@ -164,9 +167,59 @@ public void testSetFields () { assertEquals("Entry 2", e1.getPath()); } + @Test + public void testTimes() { + long beforeSecond = Instant.now().toEpochMilli()/1000; + E entry = database.newEntry(); + long afterSecond = Instant.now().toEpochMilli()/1000; + long createdSecond = entry.getCreationTime().getTime()/1000; + + assertTrue(createdSecond >= beforeSecond && createdSecond <= afterSecond); + assertFalse(entry.getExpires()); + assertTrue(entry.getLastAccessTime().getTime()/1000 <= createdSecond); + assertTrue(entry.getLastModificationTime().getTime()/1000 <= createdSecond); + + entry.setExpires(true); + entry.setExpiryTime(new Date(createdSecond*1000)); + + assertTrue(entry.getExpires()); + assertEquals(createdSecond, entry.getExpiryTime().getTime()/1000); + + + } + + @Test + public void checkAddChangeRemoveProperty() { + // only applies to databases that support arbitrary properties + E entry = database.newEntry(); + assertEquals(Entry.STANDARD_PROPERTY_NAMES.size(), entry.getPropertyNames().size()); + try { + entry.setProperty("test", "test1"); + } catch (UnsupportedOperationException e) { + if (!database.supportsNonStandardPropertyNames()) { + return; + } + fail("Database must report that it doesn't support non standrad properties"); + } + assertEquals("test1", entry.getProperty("test")); + entry.setProperty("test", "test2"); + assertEquals("test2", entry.getProperty("test")); + assertTrue(entry.removeProperty("test")); + assertFalse(entry.removeProperty("test")); + assertFalse(entry.removeProperty("test-test")); + assertEquals(Entry.STANDARD_PROPERTY_NAMES.size(), entry.getPropertyNames().size()); + try { + entry.removeProperty(Entry.STANDARD_PROPERTY_NAME_USER_NAME); + fail("Should not be able to remove standard property"); + } catch (IllegalArgumentException ignore) { + // ignore as expected + } + } + + @Test public void testNewEntry() { - Entry e2 = database.newEntry(); + E e2 = database.newEntry(); Assert.assertNull(e2.getParent()); assertEquals("", e2.getPassword()); Assert.assertNotNull(e2.getUuid()); @@ -182,18 +235,18 @@ public void testNewEntry() { @Test public void testCopy() throws IOException { - Entry entry1 = database.newEntry(); + E entry1 = database.newEntry(); entry1.setTitle("Entry"); entry1.setUsername("Username"); entry1.setPassword("Password"); - entry1.setUrl("http://dont.follow.me"); + entry1.setUrl("https://dont.follow.me"); entry1.setNotes("Notes"); entry1.setIcon(database.newIcon(2)); // create a new Database - Database database2 = createDatabase(); + Database database2 = createDatabase(); // create a new Entry in new Database - Entry entry2 = database2.newEntry(entry1); + E entry2 = database2.newEntry(entry1); assertEquals(entry1.getTitle(), entry2.getTitle()); assertEquals(entry1.getUsername(), entry2.getUsername()); @@ -203,11 +256,11 @@ public void testCopy() throws IOException { assertEquals(entry1.getIcon(), entry2.getIcon()); assertNotEquals(entry1.getUuid(), entry2.getUuid()); - Group group1 = database.newGroup(); + G group1 = database.newGroup(); group1.setName("Group"); group1.setIcon(database.newIcon(3)); - Group group2 = database2.newGroup(group1); + G group2 = database2.newGroup(group1); assertEquals(group1.getName(), group2.getName()); assertEquals(group1.getIcon(), group2.getIcon()); assertNotEquals(group1.getUuid(), group2.getUuid()); diff --git a/test/src/test/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java b/test/src/test/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java new file mode 100644 index 00000000..92998e9b --- /dev/null +++ b/test/src/test/java/org/linguafranca/pwdb/checks/BinaryPropertyChecks.java @@ -0,0 +1,193 @@ +/* + * Copyright 2015 Jo Rabin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.checks; + +import com.google.common.io.ByteStreams; +import org.junit.Assert; +import org.junit.Test; +import org.linguafranca.pwdb.*; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.*; + +/** + * Tests to verify that binary properties work correctly. Resources associated with this module contain files + * named "Attachment*.kdbx" which contain two entries: + * + *

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

+ * Subclasses should test both V3 KDBX files (Attachment.kdbx) and V4 (V4-ChaCha20-Argon2-Attachment.kdbx) since + * attachments are handled differently in the two versions. + * + * @author jo + */ +public abstract class BinaryPropertyChecks , G extends Group, E extends Entry, I extends Icon> { + + public D database; + + @SuppressWarnings("unused") + public abstract void saveDatabase(D database, Credentials credentials, OutputStream outputStream) throws IOException; + public abstract D loadDatabase(Credentials credentials, InputStream inputStream) throws IOException; + public abstract D newDatabase(); + public abstract Credentials getCreds(byte[] creds); + + /** + * Retrieve and verify attachment "letter J" + */ + @Test + public void getBinaryProperty() throws Exception { + E entry = database.findEntries("Test attachment").get(0); + byte [] letterJ = entry.getBinaryProperty("letter J.jpeg"); + InputStream testFile = getClass().getClassLoader().getResourceAsStream("letter J.jpeg"); + assert testFile != null; + byte [] original = ByteStreams.toByteArray(testFile); + Assert.assertArrayEquals(original, letterJ); + } + + /** + * Retrieve and verify attachment "letter L" + */ + @Test + public void getAnotherBinaryProperty() throws Exception { + E entry = database.findEntries("Test 2 attachment").get(0); + byte [] letterL = entry.getBinaryProperty("letter L.jpeg"); + InputStream testFile = getClass().getClassLoader().getResourceAsStream("letter L.jpeg"); + assert testFile != null; + byte [] original = ByteStreams.toByteArray(testFile); + Assert.assertArrayEquals(original, letterL); + } + + /** + * Add the Letter L to the entry containing letter J + */ + @Test + public void setBinaryProperty() throws Exception { + InputStream testFile = getClass().getClassLoader().getResourceAsStream("letter L.jpeg"); + assert testFile != null; + byte [] original = ByteStreams.toByteArray(testFile); + E entry = database.findEntries("Test attachment").get(0); + entry.setBinaryProperty("letter L.jpeg", original); + byte [] letterL = entry.getBinaryProperty("letter L.jpeg"); + Assert.assertArrayEquals(original, letterL); + assertArrayEquals(new String[] {"letter J.jpeg", "letter L.jpeg"}, entry.getBinaryPropertyNames().toArray()); + Assert.assertTrue(database.isDirty()); + } + + /** + * Verify that the entries have the right attachments + */ + @Test + public void getBinaryPropertyNames() { + E entry = database.findEntries("Test attachment").get(0); + assertArrayEquals(new String[] {"letter J.jpeg"}, entry.getBinaryPropertyNames().toArray()); + + entry = database.findEntries("Test 2 attachment").get(0); + assertArrayEquals(new String[] {"letter J.jpeg", "letter L.jpeg"}, entry.getBinaryPropertyNames().toArray()); + } + + /** + * Verify that the database correctly reports that its supports binary attachments - override for + * databases that don't + */ + @Test + public void checkSupported(){ + assertTrue(database.supportsBinaryProperties()); + } + + /** + * Verify that the binary properties can be added and removed correctly + */ + @Test + public void checkAddChangeRemoveBinaryProperty() { + byte[] test = new byte[] {0, 1, 2 ,3}; + byte[] test2 = new byte[] {3, 2, 1, 0}; + E entry = database.findEntries("Test attachment").get(0); + assertEquals(1, entry.getBinaryPropertyNames().size()); + entry.setBinaryProperty("test", test); + assertArrayEquals(test, entry.getBinaryProperty("test")); + entry.setBinaryProperty("test", test2); + assertArrayEquals(test2, entry.getBinaryProperty("test")); + // true that property was removed + assertTrue(entry.removeBinaryProperty("test")); + // false that same property was removed + assertFalse(entry.removeBinaryProperty("test")); + // false that non-existent was removed + assertFalse(entry.removeBinaryProperty("test-test")); + // same number of properties as we started with + assertEquals(1, entry.getBinaryPropertyNames().size()); + } + + /** + * Checks that a database with binary properties saves and reloads correctly + */ + @Test + public void saveAndReloadCheck() throws IOException { + Path file = Files.createTempFile("keepass", "tmp"); + saveDatabase(database, getCreds("123".getBytes()), Files.newOutputStream(file)); + + D db = loadDatabase(getCreds("123".getBytes()),Files.newInputStream(file)); + E newEntry = db.findEntries("Test attachment").get(0); + assertArrayEquals(new String[] {"letter J.jpeg"}, newEntry.getBinaryPropertyNames().toArray()); + E oldEntry = database.findEntries("Test attachment").get(0); + assertArrayEquals(oldEntry.getBinaryProperty("letter J.jpeg"), newEntry.getBinaryProperty("letter J.jpeg")); + + newEntry = db.findEntries("Test 2 attachment").get(0); + assertArrayEquals(new String[] {"letter J.jpeg", "letter L.jpeg"}, newEntry.getBinaryPropertyNames().toArray()); + oldEntry = database.findEntries("Test 2 attachment").get(0); + assertArrayEquals(oldEntry.getBinaryProperty("letter J.jpeg"), newEntry.getBinaryProperty("letter J.jpeg")); + assertArrayEquals(oldEntry.getBinaryProperty("letter L.jpeg"), newEntry.getBinaryProperty("letter L.jpeg")); + + } + + /** + * Checks that a new database can add binary properties saves and reloads correctly + */ + @Test + public void createAndSaveCheck() throws IOException { + Path file = Files.createTempFile("keepass", "tmp"); + D database1 = newDatabase(); + E entry = database1.newEntry("Test attachment"); + database1.getRootGroup().addEntry(entry); + InputStream testFile = getClass().getClassLoader().getResourceAsStream("letter J.jpeg"); + assert testFile != null; + byte [] letterJ = ByteStreams.toByteArray(testFile); + entry.setBinaryProperty("letter J.jpeg", letterJ); + saveDatabase(database1, getCreds("123".getBytes()), Files.newOutputStream(file)); + + D db = loadDatabase(getCreds("123".getBytes()),Files.newInputStream(file)); + E entry1 = db.findEntries("Test attachment").get(0); + // just one property + assertArrayEquals(new String[] {"letter J.jpeg"}, entry1.getBinaryPropertyNames().toArray()); + // content is correct + assertArrayEquals(letterJ, entry1.getBinaryProperty("letter J.jpeg")); + } + + +} diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java b/test/src/test/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java similarity index 60% rename from test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java rename to test/src/test/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java index ebddca74..32de1949 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java +++ b/test/src/test/java/org/linguafranca/pwdb/checks/DatabaseLoaderChecks.java @@ -18,39 +18,45 @@ import org.junit.Assert; import org.junit.Test; -import org.linguafranca.pwdb.Database; -import org.linguafranca.pwdb.Entry; -import org.linguafranca.pwdb.Visitor; +import org.linguafranca.pwdb.*; +import java.io.PrintStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.List; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo */ -public abstract class DatabaseLoaderChecks { - protected Database database; +public abstract class DatabaseLoaderChecks , G extends Group, E extends Entry, I extends Icon>{ + static PrintStream printStream = getTestPrintStream(); + + protected Database database; + protected boolean skipDateCheck = false; + private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssX"); /** * a test123 file for each format. Should contain the same thing. This is a basic sanity check. */ @Test - public void test123File() { + public void test123File() throws ParseException { // visit all groups and entries and list them to console - database.visit(new Visitor.Print()); + database.visit(new Visitor.Print(printStream)); // find all entries in the database // the kdb version has three additional system related entries - List anything = database.findEntries(""); + List anything = database.findEntries(""); Assert.assertTrue(10 <= anything.size()); // find all entries in the database that have the string "test" in them - List tests = database.findEntries("test"); + List tests = database.findEntries("test"); for (Entry tes: tests) { - System.out.println(tes.getTitle()); + printStream.println(tes.getTitle()); } Assert.assertEquals(4, tests.size()); if (tests.size() > 0) { @@ -59,7 +65,7 @@ public void test123File() { /* StringSelection selection = new StringSelection(pass); Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, selection); - System.out.println(pass + " copied to clip board"); + printStream.println(pass + " copied to clip board"); */ // all the relevant entries should have the password 123 String pass2 = tests.get(0).getPassword(); @@ -67,14 +73,14 @@ public void test123File() { Assert.assertEquals("123", pass2); } - List passwords = database.findEntries("password"); + List passwords = database.findEntries("password"); Assert.assertEquals(4, passwords.size()); for (Entry passwordEntry : passwords) { assertEquals(passwordEntry.getTitle(), passwordEntry.getPassword()); - System.out.println(passwordEntry.getTitle()); + printStream.println(passwordEntry.getTitle()); } - List entries = database.findEntries(new Entry.Matcher() { + List entries = database.findEntries(new Entry.Matcher() { @Override public boolean matches(Entry entry) { return entry.getTitle().equals("hello world"); @@ -82,5 +88,14 @@ public boolean matches(Entry entry) { Assert.assertEquals(1, entries.size()); assertEquals("pass", entries.get(0).getPassword()); + + // kdb files don't have a time zone so can't make head or tail of the date - test file seems to have a local time in it + if (skipDateCheck) { + return; + } + + Date c = entries.get(0).getCreationTime(); + Date expected = sdf.parse("2015-10-24T17:20:41Z"); + Assert.assertEquals(expected, c); } } diff --git a/test/src/test/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java b/test/src/test/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java new file mode 100644 index 00000000..ec9a153a --- /dev/null +++ b/test/src/test/java/org/linguafranca/pwdb/checks/PropertyValueChecks.java @@ -0,0 +1,174 @@ +/* + * Copyright 2015 Jo Rabin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.linguafranca.pwdb.checks; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.linguafranca.pwdb.*; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import static org.junit.Assert.*; + +/** + * Testing the operation of PropertyValue mechanisms + */ +public abstract class PropertyValueChecks, G extends Group, + E extends Entry, I extends Icon> { + + public abstract void saveDatabase(D database, Credentials credentials, OutputStream outputStream) throws IOException; + public abstract D loadDatabase(Credentials credentials, InputStream inputStream) throws IOException; + public abstract Credentials getCreds(byte[] creds); + + private final boolean propertyValueSupported; + protected D database; + + @BeforeClass + public static void ensureOutputDir() throws IOException { + Files.createDirectories(Paths.get("testOutput")); + } + + public PropertyValueChecks(boolean propertyValueSupported) throws IOException { + this.database = createDatabase(); + this.propertyValueSupported = propertyValueSupported; + } + + public abstract D createDatabase() throws IOException; + + /** + * Catches expected thrown exception + * + * @param f a Consumer i.e. a function that does have output + * @param c the exception that is expected to be thrown when the function is executed + */ + private void catchException(Consumer f, @SuppressWarnings("SameParameterValue") Class c) { + try { + f.accept(null); + } catch (Exception e) { + if (c.equals(e.getClass())) { + return; + } + } + fail(c.getName() + " expected"); + } + + /** + * Check that unsupported operations throw exceptions + */ + @Test + public void noPropertyValueSupport() { + if (propertyValueSupported) return; + + assertFalse(database.supportsPropertyValueStrategy()); + // test a sample of the methods that are supposed to thrown if used here + catchException((z) -> database.setPropertyValueStrategy(null), UnsupportedOperationException.class); + catchException((z) -> database.getPropertyValueStrategy(), UnsupportedOperationException.class); + catchException((z) -> database.setShouldProtect(null, true), UnsupportedOperationException.class); + catchException((z) -> database.listShouldProtect(), UnsupportedOperationException.class); + + E entry = database.newEntry(); + catchException((z) -> entry.getPropertyValue(null), UnsupportedOperationException.class); + catchException((z) -> entry.setPropertyValue(null, (PropertyValue) null), UnsupportedOperationException.class); + + } + + @Test + public void checkDefaults(){ + // these are the defaults established by createDatabase() + assertTrue(database.shouldProtect(Entry.STANDARD_PROPERTY_NAME_PASSWORD)); + assertFalse(database.shouldProtect(Entry.STANDARD_PROPERTY_NAME_USER_NAME)); + assertFalse(database.shouldProtect(Entry.STANDARD_PROPERTY_NAME_NOTES)); + assertFalse(database.shouldProtect(Entry.STANDARD_PROPERTY_NAME_URL)); + assertFalse(database.shouldProtect(Entry.STANDARD_PROPERTY_NAME_TITLE)); + } + + @Test + public void expectedStorageType() throws IOException { + if (!propertyValueSupported) { + return; + } + + // Password is protected + PropertyValue.Strategy pvs = database.getPropertyValueStrategy(); + PropertyValue pv = pvs.getFactoryFor("Password").of("123"); + assertEquals(pv.getClass(), pvs.newProtected().of("a").getClass()); + assertTrue(pv.isProtected()); + + // Notes isn't protected + pv = pvs.getFactoryFor("Notes").of("123"); + assertEquals(pv.getClass(), pvs.newUnprotected().of("b").getClass()); + assertFalse(pv.isProtected()); + + // "random" is not protected + pv = pvs.getFactoryFor("random").of("123"); + assertEquals(pv.getClass(), pvs.newUnprotected().of("b").getClass()); + assertFalse(pv.isProtected()); + + // create a strategy that makes "random" protected + PropertyValue.Strategy newPvs = new PropertyValue.Strategy() { + @Override + public List getProtectedProperties() { + List pp = new ArrayList<>(pvs.getProtectedProperties()); + pp.add("random"); + return pp; + } + + @Override + public PropertyValue.Factory newProtected() { + return pvs.newProtected(); + } + + @Override + public PropertyValue.Factory newUnprotected() { + return pvs.newUnprotected(); + } + }; + database.setPropertyValueStrategy(newPvs); + + // get a value for random and check it is protected + pv = newPvs.getFactoryFor("random").of("123"); + assertEquals(pv.getClass(), pvs.newProtected().of("b").getClass()); + assertTrue(pv.isProtected()); + + // create an entry with random property as protected value + E entry = database.newEntry("Test Random"); + entry.setPropertyValue("random", pv); + database.getRootGroup().addEntry(entry); + + // Save database + + FileOutputStream fos = new FileOutputStream("testOutput/test9.kdbx"); + saveDatabase(database, getCreds("123".getBytes()), fos); + fos.flush(); + fos.close(); + + // reload database, "random" is still protected even though it's not protected by default + FileInputStream fis = new FileInputStream("testOutput/test9.kdbx"); + D input = loadDatabase(getCreds("123".getBytes()), fis); + + List entries = input.findEntries("random"); + assertEquals(1, entries.size()); + assertTrue(entries.get(0).getPropertyValue("random").isProtected()); + assertFalse(input.getPropertyValueStrategy().getProtectedProperties().contains("random")); +} + +} diff --git a/test/src/test/java/org/linguafranca/pwdb/checks/RecycleBinChecks.java b/test/src/test/java/org/linguafranca/pwdb/checks/RecycleBinChecks.java new file mode 100644 index 00000000..3fb007ae --- /dev/null +++ b/test/src/test/java/org/linguafranca/pwdb/checks/RecycleBinChecks.java @@ -0,0 +1,79 @@ +package org.linguafranca.pwdb.checks; + +import org.junit.Test; +import org.linguafranca.pwdb.Database; +import org.linguafranca.pwdb.Entry; +import org.linguafranca.pwdb.Group; +import org.linguafranca.pwdb.Icon; + +import static org.junit.Assert.*; + +/** + * @author jo + */ +public abstract class RecycleBinChecks , G extends Group, E extends Entry, I extends Icon> { + + protected Database database; + + @Test + public void recycleBinEntries() { + database.enableRecycleBin(false); + assertFalse(database.isRecycleBinEnabled()); + assertNull(database.getRecycleBin()); + database.enableRecycleBin(true); + assertTrue(database.isRecycleBinEnabled()); + G recycleBin = database.getRecycleBin(); + assertNotNull(recycleBin); + + E entry = database.newEntry(); + assertEquals(entry, database.getRootGroup().addEntry(entry)); + assertEquals(1, database.getRootGroup().getEntriesCount()); + + assertTrue(database.deleteEntry(entry.getUuid())); + assertEquals(0, database.getRootGroup().getEntriesCount()); + assertEquals(1, recycleBin.getEntriesCount()); + + database.enableRecycleBin(false); + assertFalse(database.isRecycleBinEnabled()); + + entry = database.newEntry(); + assertEquals(entry, database.getRootGroup().addEntry(entry)); + assertEquals(1, database.getRootGroup().getEntriesCount()); + + assertTrue(database.deleteEntry(entry.getUuid())); + assertEquals(0, database.getRootGroup().getEntriesCount()); + assertEquals(1, recycleBin.getEntriesCount()); + database.emptyRecycleBin(); + assertEquals(0, recycleBin.getEntriesCount()); + } + + @Test + public void recycleBinGroups() { + database.enableRecycleBin(true); + assertTrue(database.isRecycleBinEnabled()); + G recycleBin = database.getRecycleBin(); + assertNotNull(recycleBin); + + G group= database.newGroup(); + assertEquals(group, database.getRootGroup().addGroup(group)); + assertEquals(2, database.getRootGroup().getGroupsCount()); + + assertTrue(database.deleteGroup(group.getUuid())); + assertEquals(1, database.getRootGroup().getGroupsCount()); + assertEquals(1, recycleBin.getGroupsCount()); + + database.enableRecycleBin(false); + assertFalse(database.isRecycleBinEnabled()); + + group = database.newGroup(); + assertEquals(group, database.getRootGroup().addGroup(group)); + assertEquals(2, database.getRootGroup().getGroupsCount()); + + assertTrue(database.deleteGroup(group.getUuid())); + assertEquals(1, database.getRootGroup().getGroupsCount()); + assertEquals(1, recycleBin.getGroupsCount()); + database.emptyRecycleBin(); + assertEquals(0, recycleBin.getGroupsCount()); + } + +} diff --git a/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java b/test/src/test/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java similarity index 75% rename from test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java rename to test/src/test/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java index ef42c037..a0e05a51 100644 --- a/test/src/main/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java +++ b/test/src/test/java/org/linguafranca/pwdb/checks/SaveAndReloadChecks.java @@ -28,71 +28,70 @@ import java.util.UUID; import static org.junit.Assert.*; +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; /** * @author jo */ public abstract class SaveAndReloadChecks , G extends Group, E extends Entry, I extends Icon>{ + static PrintStream printStream = getTestPrintStream(); + public abstract D getDatabase(); public abstract D getDatabase(String name, Credentials credentials) throws IOException; public abstract void saveDatabase(D database, Credentials credentials, OutputStream outputStream) throws IOException; public abstract D loadDatabase(Credentials credentials, InputStream inputStream) throws IOException; public abstract Credentials getCreds(byte[] creds); + public abstract boolean verifyStreamFormat (StreamFormat s1, StreamFormat s2); @BeforeClass public static void ensureOutputDir() throws IOException { Files.createDirectories(Paths.get("testOutput")); } + /** + * Test verifies that entries contain the same content on reload as they did on save, + * and also verifies that saving doesn't alter the contents + */ @Test public void saveAndReloadTest() throws IOException { long now = System.currentTimeMillis(); + // create database with known content D output = createNewDatabase(); - Assert.assertTrue(output.isDirty()); - assertEquals(5, output.getRootGroup().getGroupsCount()); + verifyContents(output); + //output.save(new StreamFormat.None(), new Credentials.None(), printStream); FileOutputStream fos = new FileOutputStream("testOutput/test1.kdbx"); saveDatabase(output, getCreds("123".getBytes()), fos); Assert.assertFalse(output.isDirty()); fos.flush(); fos.close(); + // make sure that saving didn't mess up content + verifyContents(output); + //output.save(new StreamFormat.None(), new Credentials.None(), printStream); + FileInputStream fis = new FileInputStream("testOutput/test1.kdbx"); D input = loadDatabase(getCreds("123".getBytes()), fis); - - for (Integer g = 0; g< 5; g++){ - Group group = input.getRootGroup().getGroups().get(g); - assertEquals(g.toString(), group.getName()); - assertEquals(g + 1, group.getEntries().size()); - assertEquals(g+1, group.getEntriesCount()); - assertEquals(input.getRootGroup(), group.getParent()); - for (int e = 0; e <= g; e++) { - Entry entry = (Entry) group.getEntries().get(e); - assertEquals(g + "-" + e, entry.getTitle()); - assertEquals(g + " - un - " + e, entry.getUsername()); - assertEquals(g + "- p -" + e, entry.getPassword()); - assertEquals(g + "- url - " + e, entry.getUrl()); - assertEquals(g + "- n - " + e, entry.getNotes()); - assertEquals(group, entry.getParent()); - } - } - //saveDatabase(input, new StreamFormat.None(), new Credentials.None(), System.out); - System.out.format("Test took %d millis", System.currentTimeMillis() - now); - + verifyContents(input); + //input.save(new StreamFormat.None(), new Credentials.None(), printStream); + printStream.format("Test took %d millis", System.currentTimeMillis() - now); } + /** + * Test verifies that attachments are saved and reloaded correctly + */ @Test public void saveAndReloadTest2() throws IOException { D attachment = getDatabase("Attachment.kdbx", getCreds("123".getBytes())); - Entry entry = attachment.findEntries("Test attachment").get(0); + E entry = attachment.findEntries("Test attachment").get(0); assertArrayEquals(new String[] {"letter J.jpeg"}, entry.getBinaryPropertyNames().toArray()); - Entry entry2 = attachment.findEntries("Test 2 attachment").get(0); + E entry2 = attachment.findEntries("Test 2 attachment").get(0); assertArrayEquals(new String[] {"letter J.jpeg", "letter L.jpeg"}, entry2.getBinaryPropertyNames().toArray()); byte[] content = entry2.getBinaryProperty("letter L.jpeg"); @@ -105,20 +104,41 @@ public void saveAndReloadTest2() throws IOException { fos.close(); FileInputStream fis = new FileInputStream("testOutput/test2.kdbx"); - Database input = loadDatabase(getCreds("123".getBytes()), fis); + D input = loadDatabase(getCreds("123".getBytes()), fis); - entry = (Entry) input.findEntries("Test attachment").get(0); + entry = input.findEntries("Test attachment").get(0); assertArrayEquals(new String[] {"letter J.jpeg", "letter L.jpeg"}, entry.getBinaryPropertyNames().toArray()); + //saveDatabase(input, new StreamFormat.None(), new Credentials.None(), printStream); + } + + String [] testFiles = {"V4-AES-AES.kdbx", + "V4-AES-Argon2.kdbx", + "V4-ChaCha20-AES.kdbx", + "V4-ChaCha20-Argon2-Attachment.kdbx"}; + + /*** + * Test verifies that database is saved with same encryption that it was loaded with + */ + @Test + public void saveAndReloadTest3() throws IOException { + for (String resource: testFiles) { + D database = getDatabase(resource, this.getCreds("123".getBytes())); + StreamFormat format1 = database.getStreamFormat(); - //saveDatabase(input, new StreamFormat.None(), new Credentials.None(), System.out); + database.save(getCreds("123".getBytes()), Files.newOutputStream(Paths.get("testOutput/test3.kdbx"))); + FileInputStream fis = new FileInputStream("testOutput/test3.kdbx"); + D input = loadDatabase(getCreds("123".getBytes()), fis); + StreamFormat format2 = input.getStreamFormat(); + assertTrue(verifyStreamFormat(format1, format2)); + } } private D createNewDatabase() throws IOException { D database = getDatabase(); - for (Integer g = 0; g < 5; g++){ + for (@SuppressWarnings("WrapperTypeMayBePrimitive") Integer g = 0; g < 5; g++){ G group = database.getRootGroup().addGroup(database.newGroup(g.toString())); for (int e = 0; e <= g; e++) { group.addEntry(entryFactory(database, g.toString(), e)); @@ -138,6 +158,25 @@ private E entryFactory(D database, String g, int e) { return result; } + private void verifyContents(D database) { + for (Integer g = 0; g< 5; g++){ + G group = database.getRootGroup().getGroups().get(g); + assertEquals(g.toString(), group.getName()); + assertEquals(g + 1, group.getEntries().size()); + assertEquals(g+1, group.getEntriesCount()); + assertEquals(database.getRootGroup(), group.getParent()); + for (int e = 0; e <= g; e++) { + E entry = group.getEntries().get(e); + assertEquals(g + "-" + e, entry.getTitle()); + assertEquals(g + " - un - " + e, entry.getUsername()); + assertEquals(g + "- p -" + e, entry.getPassword()); + assertEquals(g + "- url - " + e, entry.getUrl()); + assertEquals(g + "- n - " + e, entry.getNotes()); + assertEquals(group, entry.getParent()); + } + } + } + /** * Outputs the database to a file - we can try to read it in other versions of the program. Run "manually". * @@ -155,11 +194,11 @@ public void saveNewDatabase () throws IOException { * Doesn't do anything other than output the database using default PrintVisitor * @throws IOException when naughty */ - @Test + @Test @Ignore public void inspectNewDatabase () throws IOException { - Database database = createNewDatabase(); + D database = createNewDatabase(); - database.visit(new Visitor.Print()); + database.visit(new Visitor.Print(printStream)); } // create a new database for messing around with @@ -199,7 +238,7 @@ public void testNewDatabase() throws IOException { root.addGroup(group1); root.removeGroup(group1); - Assert.assertTrue(group1.getParent() == null); + assertNull(group1.getParent()); assertEquals(0, root.getGroups().size()); root.addGroup(group1); assertEquals(1, root.getGroups().size()); diff --git a/test/src/main/resources/Attachment.kdbx b/test/src/test/resources/Attachment.kdbx similarity index 100% rename from test/src/main/resources/Attachment.kdbx rename to test/src/test/resources/Attachment.kdbx diff --git a/test/src/main/resources/Attachment.xml b/test/src/test/resources/Attachment.xml similarity index 99% rename from test/src/main/resources/Attachment.xml rename to test/src/test/resources/Attachment.xml index e400d526..5bdd5620 100755 --- a/test/src/main/resources/Attachment.xml +++ b/test/src/test/resources/Attachment.xml @@ -1,157 +1,157 @@ - - - - KeePass - - 2016-08-27T14:41:45Z - - 2016-08-27T14:41:45Z - - 2016-08-27T14:41:45Z - 365 - - 2016-08-27T14:41:58Z - -1 - -1 - - False - False - True - False - False - - True - AAAAAAAAAAAAAAAAAAAAAA== - 2016-08-27T14:41:45Z - AAAAAAAAAAAAAAAAAAAAAA== - 2016-08-27T14:41:45Z - 10 - 6291456 - AAAAAAAAAAAAAAAAAAAAAA== - AAAAAAAAAAAAAAAAAAAAAA== - - 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 - - - - - - TOEWAvT9GlIjOkSMv9F+aw== - Root - - 48 - - 2016-08-27T14:41:45Z - 2016-08-27T14:41:45Z - 2016-08-27T14:41:45Z - 2016-08-27T14:41:45Z - False - 0 - 2016-08-27T14:41:45Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - GPv+n0B4upj0sZd+nuuiBQ== - 0 - - - - - - 2016-08-27T14:42:09Z - 2016-08-27T14:43:51Z - 2016-09-11T11:11:59Z - 2016-08-27T14:42:09Z - False - 1 - 2016-08-27T14:43:51Z - - - Notes - - - - Password - - - - Title - Test attachment - - - URL - - - - UserName - - - - letter J.jpeg - - - - True - 0 - - - - - mwq3nFNpPrvjXlsWy/6FTQ== - 0 - - - - - - 2016-08-27T15:03:52Z - 2016-08-27T15:04:44Z - 2016-08-27T15:04:44Z - 2016-08-27T15:03:52Z - False - 0 - 2016-08-27T15:04:44Z - - - Notes - - - - Password - - - - Title - Test 2 Attachments - - - URL - - - - UserName - - - - letter J.jpeg - - - - letter L.jpeg - - - - True - 0 - - - - - - + + + + KeePass + + 2016-08-27T14:41:45Z + + 2016-08-27T14:41:45Z + + 2016-08-27T14:41:45Z + 365 + + 2016-08-27T14:41:58Z + -1 + -1 + + False + False + True + False + False + + True + AAAAAAAAAAAAAAAAAAAAAA== + 2016-08-27T14:41:45Z + AAAAAAAAAAAAAAAAAAAAAA== + 2016-08-27T14:41:45Z + 10 + 6291456 + AAAAAAAAAAAAAAAAAAAAAA== + AAAAAAAAAAAAAAAAAAAAAA== + + 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 + + + + + + TOEWAvT9GlIjOkSMv9F+aw== + Root + + 48 + + 2016-08-27T14:41:45Z + 2016-08-27T14:41:45Z + 2016-08-27T14:41:45Z + 2016-08-27T14:41:45Z + False + 0 + 2016-08-27T14:41:45Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + GPv+n0B4upj0sZd+nuuiBQ== + 0 + + + + + + 2016-08-27T14:42:09Z + 2016-08-27T14:43:51Z + 2016-09-11T11:11:59Z + 2016-08-27T14:42:09Z + False + 1 + 2016-08-27T14:43:51Z + + + Notes + + + + Password + + + + Title + Test attachment + + + URL + + + + UserName + + + + letter J.jpeg + + + + True + 0 + + + + + mwq3nFNpPrvjXlsWy/6FTQ== + 0 + + + + + + 2016-08-27T15:03:52Z + 2016-08-27T15:04:44Z + 2016-08-27T15:04:44Z + 2016-08-27T15:03:52Z + False + 0 + 2016-08-27T15:04:44Z + + + Notes + + + + Password + + + + Title + Test 2 Attachments + + + URL + + + + UserName + + + + letter J.jpeg + + + + letter L.jpeg + + + + True + 0 + + + + + + \ No newline at end of file diff --git a/test/src/test/resources/Database-4.1-123.kdbx b/test/src/test/resources/Database-4.1-123.kdbx new file mode 100644 index 00000000..024017e1 Binary files /dev/null and b/test/src/test/resources/Database-4.1-123.kdbx differ diff --git a/test/src/main/resources/EmptyPassword.kdbx b/test/src/test/resources/EmptyPassword.kdbx similarity index 100% rename from test/src/main/resources/EmptyPassword.kdbx rename to test/src/test/resources/EmptyPassword.kdbx diff --git a/test/src/main/resources/EmptyPasswordWithKey.kdbx b/test/src/test/resources/EmptyPasswordWithKey.kdbx similarity index 100% rename from test/src/main/resources/EmptyPasswordWithKey.kdbx rename to test/src/test/resources/EmptyPasswordWithKey.kdbx diff --git a/test/src/main/resources/EmptyPasswordWithKey.key b/test/src/test/resources/EmptyPasswordWithKey.key similarity index 100% rename from test/src/main/resources/EmptyPasswordWithKey.key rename to test/src/test/resources/EmptyPasswordWithKey.key diff --git a/test/src/main/resources/ExampleDatabase.kdbx b/test/src/test/resources/ExampleDatabase.kdbx similarity index 100% rename from test/src/main/resources/ExampleDatabase.kdbx rename to test/src/test/resources/ExampleDatabase.kdbx diff --git a/test/src/main/resources/ExampleDatabase.xml b/test/src/test/resources/ExampleDatabase.xml similarity index 99% rename from test/src/main/resources/ExampleDatabase.xml rename to test/src/test/resources/ExampleDatabase.xml index c7367899..6535b834 100755 --- a/test/src/main/resources/ExampleDatabase.xml +++ b/test/src/test/resources/ExampleDatabase.xml @@ -1,583 +1,583 @@ - - - - KeePass - - 2016-09-10T16:29:17Z - - 2016-09-10T16:29:17Z - - 2016-09-10T16:29:17Z - 365 - #FF0000 - 2016-09-10T16:29:17Z - -1 - -1 - - False - False - True - False - False - - - - kJh5KNGNaU6SZIsfsAwx7g== - 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 - - - True - 4fkGUxKnlUae43TI8NsnLQ== - 2016-09-10T16:29:17Z - V/rJOkFjYUqrFCDdP8HeSw== - 2016-09-10T16:36:55Z - 10 - 6291456 - SkjqCvDUmU6y6k7onr6ewg== - oy863aK0cEKApjbJfGLHZQ== - - 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 - - - - - - oy863aK0cEKApjbJfGLHZQ== - ExampleDatabase - - 49 - - 2016-09-10T16:29:17Z - 2016-09-10T16:29:17Z - 2016-09-10T18:50:45Z - 2016-09-10T15:04:19Z - False - 19 - 2016-09-10T16:29:17Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - 9truGTw7i0+svm1EKjSCXQ== - 0 - - - - - - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T15:04:19Z - False - 0 - 2016-09-10T16:29:32Z - - - Notes - Notes - - - Password - Password - - - Title - Sample Entry - - - URL - http://keepass.info/ - - - UserName - User Name - - - True - 0 - - Target Window - {USERNAME}{TAB}{PASSWORD}{TAB}{ENTER} - - - - - - qgRU1hFDE0az+mXL5mTrFg== - 0 - - - - - - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T15:04:19Z - False - 0 - 2016-09-10T16:29:32Z - - - Password - 12345 - - - Title - Sample Entry #2 - - - URL - http://keepass.info/help/kb/testform.html - - - UserName - Michael321 - - - True - 0 - - *Test Form - KeePass* - - - - - - - y5JBCSQMd0qKhGu3M9XjIw== - General - - 48 - - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T18:50:41Z - 2016-09-10T15:04:19Z - False - 2 - 2016-09-10T16:29:32Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - 0w2ac9Zj6Uqp6Up46CyNhQ== - Windows - - 38 - - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T18:50:36Z - 2016-09-10T15:04:19Z - False - 2 - 2016-09-10T16:29:32Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - zlA7tzCMp0WLJD8Bjw+rCw== - Network - - 3 - - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T18:50:36Z - 2016-09-10T15:04:19Z - False - 1 - 2016-09-10T16:29:32Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - JATpnerLXU+0DtRco5cx6Q== - Internet - - 1 - - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T18:50:37Z - 2016-09-10T15:04:19Z - False - 2 - 2016-09-10T16:29:32Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - KS6v+Pw92EmIby9paykzpQ== - eMail - - 19 - - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T18:50:38Z - 2016-09-10T15:04:19Z - False - 1 - 2016-09-10T16:29:32Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - 4fkGUxKnlUae43TI8NsnLQ== - Recycle Bin - - 43 - - 2016-09-10T16:30:02Z - 2016-09-10T16:30:02Z - 2016-09-10T18:50:45Z - 2016-09-10T15:04:19Z - False - 7 - 2016-09-10T16:30:02Z - - True - - false - false - AAAAAAAAAAAAAAAAAAAAAA== - - SkjqCvDUmU6y6k7onr6ewg== - Homebanking - - 37 - - 2016-09-10T16:29:32Z - 2016-09-10T16:29:32Z - 2016-09-10T18:50:45Z - 2016-09-10T15:04:19Z - False - 8 - 2016-09-10T16:30:02Z - - True - - null - null - qYwf0cP0sU6mfMsfj+s9EA== - - qYwf0cP0sU6mfMsfj+s9EA== - 37 - kJh5KNGNaU6SZIsfsAwx7g== - - - - c;b;a - - 2016-09-10T16:29:50Z - 2016-09-11T10:44:40Z - 2016-09-11T10:44:40Z - 2016-09-10T15:04:19Z - False - 9 - 2016-09-10T16:29:50Z - - - Notes - - - - Password - 1abEUk2xWlLtfDIvzwYJ - - - Title - BinaryField - - - URL - - - - UserName - - - - letter J.jpeg - - - - True - 0 - - - - qYwf0cP0sU6mfMsfj+s9EA== - 37 - - - - - - 2016-09-10T16:29:50Z - 2016-09-10T16:29:56Z - 2016-09-10T16:29:56Z - 2016-09-10T15:04:19Z - False - 1 - 2016-09-10T16:29:50Z - - - Notes - - - - Password - 1abEUk2xWlLtfDIvzwYJ - - - Title - hhs - - - URL - - - - UserName - - - - True - 0 - - - - qYwf0cP0sU6mfMsfj+s9EA== - 37 - kJh5KNGNaU6SZIsfsAwx7g== - - - - - - 2016-09-10T16:29:50Z - 2016-09-10T17:26:56Z - 2016-09-10T17:45:44Z - 2016-09-10T15:04:19Z - False - 3 - 2016-09-10T16:29:50Z - - - Notes - - - - Password - 1abEUk2xWlLtfDIvzwYJ - - - Title - hhs - - - URL - - - - UserName - - - - True - 0 - - - - qYwf0cP0sU6mfMsfj+s9EA== - 37 - kJh5KNGNaU6SZIsfsAwx7g== - - - - - - 2016-09-10T16:29:50Z - 2016-09-10T18:39:40Z - 2016-09-10T18:40:02Z - 2016-09-10T15:04:19Z - False - 6 - 2016-09-10T16:29:50Z - - - Notes - - - - Password - 1abEUk2xWlLtfDIvzwYJ - - - Title - hhs - - - URL - - - - UserName - - - - letter J.jpeg - - - - True - 0 - - - - qYwf0cP0sU6mfMsfj+s9EA== - 37 - kJh5KNGNaU6SZIsfsAwx7g== - - - - a b c - - 2016-09-10T16:29:50Z - 2016-09-10T18:50:20Z - 2016-09-10T18:50:20Z - 2016-09-10T15:04:19Z - False - 7 - 2016-09-10T16:29:50Z - - - Notes - - - - Password - 1abEUk2xWlLtfDIvzwYJ - - - Title - hhs - - - URL - - - - UserName - - - - letter J.jpeg - - - - True - 0 - - - - qYwf0cP0sU6mfMsfj+s9EA== - 37 - kJh5KNGNaU6SZIsfsAwx7g== - - - - c;b;a - - 2016-09-10T16:29:50Z - 2016-09-10T18:51:05Z - 2016-09-10T18:51:05Z - 2016-09-10T15:04:19Z - False - 8 - 2016-09-10T16:29:50Z - - - Notes - - - - Password - 1abEUk2xWlLtfDIvzwYJ - - - Title - hhs - - - URL - - - - UserName - - - - letter J.jpeg - - - - True - 0 - - - - - - - - V/rJOkFjYUqrFCDdP8HeSw== - Templates - - 48 - - 2016-09-10T16:36:24Z - 2016-09-10T16:36:29Z - 2016-09-10T18:50:40Z - 2016-09-09T23:00:00Z - False - 4 - 2016-09-10T16:36:32Z - - True - - null - null - AAAAAAAAAAAAAAAAAAAAAA== - - - - + + + + KeePass + + 2016-09-10T16:29:17Z + + 2016-09-10T16:29:17Z + + 2016-09-10T16:29:17Z + 365 + #FF0000 + 2016-09-10T16:29:17Z + -1 + -1 + + False + False + True + False + False + + + + kJh5KNGNaU6SZIsfsAwx7g== + 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 + + + True + 4fkGUxKnlUae43TI8NsnLQ== + 2016-09-10T16:29:17Z + V/rJOkFjYUqrFCDdP8HeSw== + 2016-09-10T16:36:55Z + 10 + 6291456 + SkjqCvDUmU6y6k7onr6ewg== + oy863aK0cEKApjbJfGLHZQ== + + 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 + + + + + + oy863aK0cEKApjbJfGLHZQ== + ExampleDatabase + + 49 + + 2016-09-10T16:29:17Z + 2016-09-10T16:29:17Z + 2016-09-10T18:50:45Z + 2016-09-10T15:04:19Z + False + 19 + 2016-09-10T16:29:17Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + 9truGTw7i0+svm1EKjSCXQ== + 0 + + + + + + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T15:04:19Z + False + 0 + 2016-09-10T16:29:32Z + + + Notes + Notes + + + Password + Password + + + Title + Sample Entry + + + URL + http://keepass.info/ + + + UserName + User Name + + + True + 0 + + Target Window + {USERNAME}{TAB}{PASSWORD}{TAB}{ENTER} + + + + + + qgRU1hFDE0az+mXL5mTrFg== + 0 + + + + + + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T15:04:19Z + False + 0 + 2016-09-10T16:29:32Z + + + Password + 12345 + + + Title + Sample Entry #2 + + + URL + http://keepass.info/help/kb/testform.html + + + UserName + Michael321 + + + True + 0 + + *Test Form - KeePass* + + + + + + + y5JBCSQMd0qKhGu3M9XjIw== + General + + 48 + + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T18:50:41Z + 2016-09-10T15:04:19Z + False + 2 + 2016-09-10T16:29:32Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + 0w2ac9Zj6Uqp6Up46CyNhQ== + Windows + + 38 + + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T18:50:36Z + 2016-09-10T15:04:19Z + False + 2 + 2016-09-10T16:29:32Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + zlA7tzCMp0WLJD8Bjw+rCw== + Network + + 3 + + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T18:50:36Z + 2016-09-10T15:04:19Z + False + 1 + 2016-09-10T16:29:32Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + JATpnerLXU+0DtRco5cx6Q== + Internet + + 1 + + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T18:50:37Z + 2016-09-10T15:04:19Z + False + 2 + 2016-09-10T16:29:32Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + KS6v+Pw92EmIby9paykzpQ== + eMail + + 19 + + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T18:50:38Z + 2016-09-10T15:04:19Z + False + 1 + 2016-09-10T16:29:32Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + 4fkGUxKnlUae43TI8NsnLQ== + Recycle Bin + + 43 + + 2016-09-10T16:30:02Z + 2016-09-10T16:30:02Z + 2016-09-10T18:50:45Z + 2016-09-10T15:04:19Z + False + 7 + 2016-09-10T16:30:02Z + + True + + false + false + AAAAAAAAAAAAAAAAAAAAAA== + + SkjqCvDUmU6y6k7onr6ewg== + Homebanking + + 37 + + 2016-09-10T16:29:32Z + 2016-09-10T16:29:32Z + 2016-09-10T18:50:45Z + 2016-09-10T15:04:19Z + False + 8 + 2016-09-10T16:30:02Z + + True + + null + null + qYwf0cP0sU6mfMsfj+s9EA== + + qYwf0cP0sU6mfMsfj+s9EA== + 37 + kJh5KNGNaU6SZIsfsAwx7g== + + + + c;b;a + + 2016-09-10T16:29:50Z + 2016-09-11T10:44:40Z + 2016-09-11T10:44:40Z + 2016-09-10T15:04:19Z + False + 9 + 2016-09-10T16:29:50Z + + + Notes + + + + Password + 1abEUk2xWlLtfDIvzwYJ + + + Title + BinaryField + + + URL + + + + UserName + + + + letter J.jpeg + + + + True + 0 + + + + qYwf0cP0sU6mfMsfj+s9EA== + 37 + + + + + + 2016-09-10T16:29:50Z + 2016-09-10T16:29:56Z + 2016-09-10T16:29:56Z + 2016-09-10T15:04:19Z + False + 1 + 2016-09-10T16:29:50Z + + + Notes + + + + Password + 1abEUk2xWlLtfDIvzwYJ + + + Title + hhs + + + URL + + + + UserName + + + + True + 0 + + + + qYwf0cP0sU6mfMsfj+s9EA== + 37 + kJh5KNGNaU6SZIsfsAwx7g== + + + + + + 2016-09-10T16:29:50Z + 2016-09-10T17:26:56Z + 2016-09-10T17:45:44Z + 2016-09-10T15:04:19Z + False + 3 + 2016-09-10T16:29:50Z + + + Notes + + + + Password + 1abEUk2xWlLtfDIvzwYJ + + + Title + hhs + + + URL + + + + UserName + + + + True + 0 + + + + qYwf0cP0sU6mfMsfj+s9EA== + 37 + kJh5KNGNaU6SZIsfsAwx7g== + + + + + + 2016-09-10T16:29:50Z + 2016-09-10T18:39:40Z + 2016-09-10T18:40:02Z + 2016-09-10T15:04:19Z + False + 6 + 2016-09-10T16:29:50Z + + + Notes + + + + Password + 1abEUk2xWlLtfDIvzwYJ + + + Title + hhs + + + URL + + + + UserName + + + + letter J.jpeg + + + + True + 0 + + + + qYwf0cP0sU6mfMsfj+s9EA== + 37 + kJh5KNGNaU6SZIsfsAwx7g== + + + + a b c + + 2016-09-10T16:29:50Z + 2016-09-10T18:50:20Z + 2016-09-10T18:50:20Z + 2016-09-10T15:04:19Z + False + 7 + 2016-09-10T16:29:50Z + + + Notes + + + + Password + 1abEUk2xWlLtfDIvzwYJ + + + Title + hhs + + + URL + + + + UserName + + + + letter J.jpeg + + + + True + 0 + + + + qYwf0cP0sU6mfMsfj+s9EA== + 37 + kJh5KNGNaU6SZIsfsAwx7g== + + + + c;b;a + + 2016-09-10T16:29:50Z + 2016-09-10T18:51:05Z + 2016-09-10T18:51:05Z + 2016-09-10T15:04:19Z + False + 8 + 2016-09-10T16:29:50Z + + + Notes + + + + Password + 1abEUk2xWlLtfDIvzwYJ + + + Title + hhs + + + URL + + + + UserName + + + + letter J.jpeg + + + + True + 0 + + + + + + + + V/rJOkFjYUqrFCDdP8HeSw== + Templates + + 48 + + 2016-09-10T16:36:24Z + 2016-09-10T16:36:29Z + 2016-09-10T18:50:40Z + 2016-09-09T23:00:00Z + False + 4 + 2016-09-10T16:36:32Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + + \ No newline at end of file diff --git a/test/src/main/resources/KeyFileDatabase.kdbx b/test/src/test/resources/KeyFileDatabase.kdbx similarity index 100% rename from test/src/main/resources/KeyFileDatabase.kdbx rename to test/src/test/resources/KeyFileDatabase.kdbx diff --git a/test/src/main/resources/KeyFileDatabase.key b/test/src/test/resources/KeyFileDatabase.key similarity index 95% rename from test/src/main/resources/KeyFileDatabase.key rename to test/src/test/resources/KeyFileDatabase.key index 6f63430b..aade8cbf 100755 --- a/test/src/main/resources/KeyFileDatabase.key +++ b/test/src/test/resources/KeyFileDatabase.key @@ -1,9 +1,9 @@ - - - - 1.00 - - - uE9L7SGiT0+PrSvZ0oIzaLjtpsiu50ciR/JXc44Pw24= - - + + + + 1.00 + + + uE9L7SGiT0+PrSvZ0oIzaLjtpsiu50ciR/JXc44Pw24= + + diff --git a/test/src/main/resources/NoPasswordWithKey.kdbx b/test/src/test/resources/NoPasswordWithKey.kdbx similarity index 100% rename from test/src/main/resources/NoPasswordWithKey.kdbx rename to test/src/test/resources/NoPasswordWithKey.kdbx diff --git a/test/src/main/resources/NoPasswordWithKey.key b/test/src/test/resources/NoPasswordWithKey.key similarity index 95% rename from test/src/main/resources/NoPasswordWithKey.key rename to test/src/test/resources/NoPasswordWithKey.key index 6fdee6b8..300da4d5 100755 --- a/test/src/main/resources/NoPasswordWithKey.key +++ b/test/src/test/resources/NoPasswordWithKey.key @@ -1,9 +1,9 @@ - - - - 1.00 - - - RJS69T/8tKYwO/fXPNGdtHlgLM5VfJmBDQLOlsg1AcE= - - + + + + 1.00 + + + RJS69T/8tKYwO/fXPNGdtHlgLM5VfJmBDQLOlsg1AcE= + + diff --git a/test/src/test/resources/V3-CustomIcon.kdbx b/test/src/test/resources/V3-CustomIcon.kdbx new file mode 100644 index 00000000..b69439e4 Binary files /dev/null and b/test/src/test/resources/V3-CustomIcon.kdbx differ diff --git a/test/src/test/resources/V4-AES-AES.kdbx b/test/src/test/resources/V4-AES-AES.kdbx new file mode 100644 index 00000000..da60da3b Binary files /dev/null and b/test/src/test/resources/V4-AES-AES.kdbx differ diff --git a/test/src/test/resources/V4-AES-Argon2-CustomIcon.kdbx b/test/src/test/resources/V4-AES-Argon2-CustomIcon.kdbx new file mode 100644 index 00000000..d4a49c7a Binary files /dev/null and b/test/src/test/resources/V4-AES-Argon2-CustomIcon.kdbx differ diff --git a/test/src/test/resources/V4-AES-Argon2.kdbx b/test/src/test/resources/V4-AES-Argon2.kdbx new file mode 100755 index 00000000..5d9e5794 Binary files /dev/null and b/test/src/test/resources/V4-AES-Argon2.kdbx differ diff --git a/test/src/test/resources/V4-ChaCha20-AES.kdbx b/test/src/test/resources/V4-ChaCha20-AES.kdbx new file mode 100644 index 00000000..9f33a1f1 Binary files /dev/null and b/test/src/test/resources/V4-ChaCha20-AES.kdbx differ diff --git a/test/src/test/resources/V4-ChaCha20-Argon2-Attachment.kdbx b/test/src/test/resources/V4-ChaCha20-Argon2-Attachment.kdbx new file mode 100755 index 00000000..59b73e50 Binary files /dev/null and b/test/src/test/resources/V4-ChaCha20-Argon2-Attachment.kdbx differ diff --git a/test/src/test/resources/V4-CustomIcon.kdbx b/test/src/test/resources/V4-CustomIcon.kdbx new file mode 100644 index 00000000..51b1c47e Binary files /dev/null and b/test/src/test/resources/V4-CustomIcon.kdbx differ diff --git a/test/src/test/resources/issue-27/bogus-timestamp.kdbx b/test/src/test/resources/issue-27/bogus-timestamp.kdbx new file mode 100644 index 00000000..cdf8ad6a Binary files /dev/null and b/test/src/test/resources/issue-27/bogus-timestamp.kdbx differ diff --git a/test/src/test/resources/issue-27/bogus-timestamp2.kdbx b/test/src/test/resources/issue-27/bogus-timestamp2.kdbx new file mode 100644 index 00000000..cdf8ad6a Binary files /dev/null and b/test/src/test/resources/issue-27/bogus-timestamp2.kdbx differ diff --git a/test/src/test/resources/issue-38/Database/Database.kdbx b/test/src/test/resources/issue-38/Database/Database.kdbx new file mode 100644 index 00000000..b769f15a Binary files /dev/null and b/test/src/test/resources/issue-38/Database/Database.kdbx differ diff --git a/test/src/test/resources/issue-38/Database/Database.keyx b/test/src/test/resources/issue-38/Database/Database.keyx new file mode 100644 index 00000000..fe8be0de --- /dev/null +++ b/test/src/test/resources/issue-38/Database/Database.keyx @@ -0,0 +1,12 @@ + + + + 2.0 + + + + A700DD37 8EBAABE2 1936E1F9 9FED8DEE + D0F71664 B25B0DD2 961FFDA6 9E194E75 + + + \ No newline at end of file diff --git a/test/src/test/resources/issue-38/Database/README.md b/test/src/test/resources/issue-38/Database/README.md new file mode 100644 index 00000000..9098fa49 --- /dev/null +++ b/test/src/test/resources/issue-38/Database/README.md @@ -0,0 +1 @@ +This was rasied as Issue 38 - Keyfile is version 2, which was not supported. \ No newline at end of file diff --git a/test/src/test/resources/issue-70/test-hugoo10.kdbx b/test/src/test/resources/issue-70/test-hugoo10.kdbx new file mode 100644 index 00000000..1eb01859 Binary files /dev/null and b/test/src/test/resources/issue-70/test-hugoo10.kdbx differ diff --git a/test/src/test/resources/issue-70/test1.kdbx b/test/src/test/resources/issue-70/test1.kdbx new file mode 100644 index 00000000..d27f37d1 Binary files /dev/null and b/test/src/test/resources/issue-70/test1.kdbx differ diff --git a/test/src/test/resources/issue-70/test2.kdbx b/test/src/test/resources/issue-70/test2.kdbx new file mode 100644 index 00000000..76f7c742 Binary files /dev/null and b/test/src/test/resources/issue-70/test2.kdbx differ diff --git a/test/src/test/resources/kdb.key b/test/src/test/resources/kdb.key new file mode 100755 index 00000000..d2f12aef --- /dev/null +++ b/test/src/test/resources/kdb.key @@ -0,0 +1 @@ +a320d61f69f0903d9f4cecb95c81b35f77b1af232a0b8f2d84377f7df57022d4 \ No newline at end of file diff --git a/test/src/test/resources/kdb_with_random_file.kdbx b/test/src/test/resources/kdb_with_random_file.kdbx new file mode 100755 index 00000000..d46ac89f Binary files /dev/null and b/test/src/test/resources/kdb_with_random_file.kdbx differ diff --git a/test/src/test/resources/kdbwithkey.kdb b/test/src/test/resources/kdbwithkey.kdb new file mode 100755 index 00000000..b69e5b70 Binary files /dev/null and b/test/src/test/resources/kdbwithkey.kdb differ diff --git a/test/src/main/resources/kdbxSample.xml b/test/src/test/resources/kdbxSample.xml similarity index 100% rename from test/src/main/resources/kdbxSample.xml rename to test/src/test/resources/kdbxSample.xml diff --git a/test/src/test/resources/kdbx_hash_test.kdbx b/test/src/test/resources/kdbx_hash_test.kdbx new file mode 100755 index 00000000..42ee0925 Binary files /dev/null and b/test/src/test/resources/kdbx_hash_test.kdbx differ diff --git a/test/src/test/resources/kdbx_hash_test.keyx b/test/src/test/resources/kdbx_hash_test.keyx new file mode 100755 index 00000000..e5254af2 --- /dev/null +++ b/test/src/test/resources/kdbx_hash_test.keyx @@ -0,0 +1,12 @@ + + + + 2.0 + + + + 66F110CA E32995B5 EFA8E672 1E70C773 + 48F8E260 EEDD8744 93F41803 5BFDC27D + + + \ No newline at end of file diff --git a/test/src/test/resources/kdbx_hash_test_wrong_hash.keyx b/test/src/test/resources/kdbx_hash_test_wrong_hash.keyx new file mode 100755 index 00000000..2bfa7c09 --- /dev/null +++ b/test/src/test/resources/kdbx_hash_test_wrong_hash.keyx @@ -0,0 +1,12 @@ + + + + 2.0 + + + + 66F110CA E32995B5 EFA8E672 1E70C773 + 48F8E260 EEDD8744 93F41803 5BFDC27D + + + \ No newline at end of file diff --git a/test/src/test/resources/kdbx_keyfile32.kdbx b/test/src/test/resources/kdbx_keyfile32.kdbx new file mode 100755 index 00000000..59dde1ba Binary files /dev/null and b/test/src/test/resources/kdbx_keyfile32.kdbx differ diff --git a/test/src/test/resources/kdbx_keyfile64.kdbx b/test/src/test/resources/kdbx_keyfile64.kdbx new file mode 100755 index 00000000..04adb615 Binary files /dev/null and b/test/src/test/resources/kdbx_keyfile64.kdbx differ diff --git a/test/src/test/resources/keyfile32 b/test/src/test/resources/keyfile32 new file mode 100755 index 00000000..70b28d84 --- /dev/null +++ b/test/src/test/resources/keyfile32 @@ -0,0 +1 @@ +KQQWFvUqScAobSfwq4z5MwnbITIb38vm \ No newline at end of file diff --git a/test/src/test/resources/keyfile64 b/test/src/test/resources/keyfile64 new file mode 100755 index 00000000..d41b5e88 --- /dev/null +++ b/test/src/test/resources/keyfile64 @@ -0,0 +1 @@ +bc63a755436af06b88027816446ba0486e8e15fac0e26dbaee82f662c77546bc \ No newline at end of file diff --git a/test/src/main/resources/letter J.jpeg b/test/src/test/resources/letter J.jpeg similarity index 100% rename from test/src/main/resources/letter J.jpeg rename to test/src/test/resources/letter J.jpeg diff --git a/test/src/main/resources/letter L.jpeg b/test/src/test/resources/letter L.jpeg similarity index 100% rename from test/src/main/resources/letter L.jpeg rename to test/src/test/resources/letter L.jpeg diff --git a/test/src/test/resources/random_file b/test/src/test/resources/random_file new file mode 100755 index 00000000..a572bb47 Binary files /dev/null and b/test/src/test/resources/random_file differ diff --git a/test/src/main/resources/test1.kdbx b/test/src/test/resources/test1.kdbx similarity index 100% rename from test/src/main/resources/test1.kdbx rename to test/src/test/resources/test1.kdbx diff --git a/test/src/test/resources/test123-AES-AES.kdbx b/test/src/test/resources/test123-AES-AES.kdbx new file mode 100755 index 00000000..2e368442 Binary files /dev/null and b/test/src/test/resources/test123-AES-AES.kdbx differ diff --git a/test/src/test/resources/test123-AES-Argon2.kdbx b/test/src/test/resources/test123-AES-Argon2.kdbx new file mode 100755 index 00000000..1f95bc43 Binary files /dev/null and b/test/src/test/resources/test123-AES-Argon2.kdbx differ diff --git a/test/src/test/resources/test123-ChaCha20-AES.kdbx b/test/src/test/resources/test123-ChaCha20-AES.kdbx new file mode 100755 index 00000000..7e45fd34 Binary files /dev/null and b/test/src/test/resources/test123-ChaCha20-AES.kdbx differ diff --git a/test/src/test/resources/test123-ChaCha20-Argon2.kdbx b/test/src/test/resources/test123-ChaCha20-Argon2.kdbx new file mode 100755 index 00000000..7722d6f2 Binary files /dev/null and b/test/src/test/resources/test123-ChaCha20-Argon2.kdbx differ diff --git a/test/src/main/resources/test123.kdb b/test/src/test/resources/test123.kdb similarity index 100% rename from test/src/main/resources/test123.kdb rename to test/src/test/resources/test123.kdb diff --git a/test/src/main/resources/test123.kdbx b/test/src/test/resources/test123.kdbx similarity index 100% rename from test/src/main/resources/test123.kdbx rename to test/src/test/resources/test123.kdbx diff --git a/test/src/test/resources/testDeleted.kdbx b/test/src/test/resources/testDeleted.kdbx new file mode 100755 index 00000000..4a5353c4 Binary files /dev/null and b/test/src/test/resources/testDeleted.kdbx differ diff --git a/test/src/test/resources/xml/V4-AES-AES.xml b/test/src/test/resources/xml/V4-AES-AES.xml new file mode 100644 index 00000000..a8cf1f9f --- /dev/null +++ b/test/src/test/resources/xml/V4-AES-AES.xml @@ -0,0 +1,319 @@ + + + + KeePass + New V4 Database + mNL70Q4AAAA= + This is a new database + mNL70Q4AAAA= + + ddH70Q4AAAA= + 365 + + ddH70Q4AAAA= + -1 + -1 + + False + False + True + False + False + + + True + AAAAAAAAAAAAAAAAAAAAAA== + ddH70Q4AAAA= + AAAAAAAAAAAAAAAAAAAAAA== + ddH70Q4AAAA= + 3mzTUVoVAkO7DFaZ6Soxrw== + 3mzTUVoVAkO7DFaZ6Soxrw== + 10 + 6291456 + vKTp2w4AAAA= + + + + 3mzTUVoVAkO7DFaZ6Soxrw== + NewV4Database + + 49 + + ddH70Q4AAAA= + ddH70Q4AAAA= + tdL70Q4AAAA= + PNH70Q4AAAA= + False + 4 + ddH70Q4AAAA= + + True + + null + null + t53t29Y1A0abjVS1+xTo5g== + + t53t29Y1A0abjVS1+xTo5g== + 0 + + + + + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + + Notes + Notes + + + Password + pWyvoOZreWQ= + + + Title + Sample Entry + + + URL + https://keepass.info/ + + + UserName + User Name + + + True + 0 + + + Target Window + {USERNAME}{TAB}{PASSWORD}{TAB}{ENTER} + + + + + + YeputjepZky3vXID4/HHrA== + 0 + + + + + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + + Notes + + + + Password + BYn/XRc= + + + Title + Sample Entry #2 + + + URL + https://keepass.info/help/kb/testform.html + + + UserName + Michael321 + + + True + 0 + + + *Test Form - KeePass* + + + + + + + TyxuU/7td0iU8+b0iHRh3w== + 0 + + + + + + mNL70Q4AAAA= + OiL90Q4AAAA= + jCL90Q4AAAA= + PNH70Q4AAAA= + False + 1 + OiL90Q4AAAA= + + + Notes + + + + Password + OvGvQqk= + + + Title + Sample Entry #2 - Copy + + + URL + https://keepass.info/help/kb/testform.html + + + UserName + Michael321 + + + True + 0 + + + *Test Form - KeePass* + + + + + + + 3BxuF+oUekuMpLe67xmTLQ== + General + + 48 + + mNL70Q4AAAA= + mNL70Q4AAAA= + tNL70Q4AAAA= + PNH70Q4AAAA= + False + 2 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + vQsKl6dJiEiNvbTkjDVHcw== + Windows + + 38 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + 6bNmbKf/QUKN7I3GlNqqQw== + Network + + 3 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + Qz8rqQvYZUummUrakV6BFQ== + Internet + + 1 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + WMP+6xrurE+Jr6kFDwJtxQ== + eMail + + 19 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + mSY4yshaIU64BNreFBm+1w== + Homebanking + + 37 + + mNL70Q4AAAA= + mNL70Q4AAAA= + s9L70Q4AAAA= + PNH70Q4AAAA= + False + 1 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + + + diff --git a/test/src/test/resources/xml/V4-AES-Argon2.xml b/test/src/test/resources/xml/V4-AES-Argon2.xml new file mode 100644 index 00000000..3ebf3a01 --- /dev/null +++ b/test/src/test/resources/xml/V4-AES-Argon2.xml @@ -0,0 +1,266 @@ + + + + KeePass + mNL70Q4AAAA= + New V4 Database + mNL70Q4AAAA= + This is a new database + mNL70Q4AAAA= + + ddH70Q4AAAA= + 365 + + ddH70Q4AAAA= + -1 + -1 + + False + False + True + False + False + + True + AAAAAAAAAAAAAAAAAAAAAA== + ddH70Q4AAAA= + AAAAAAAAAAAAAAAAAAAAAA== + ddH70Q4AAAA= + 10 + 6291456 + 3mzTUVoVAkO7DFaZ6Soxrw== + 3mzTUVoVAkO7DFaZ6Soxrw== + + + + + 3mzTUVoVAkO7DFaZ6Soxrw== + NewV4Database + + 49 + + ddH70Q4AAAA= + ddH70Q4AAAA= + tdL70Q4AAAA= + PNH70Q4AAAA= + False + 4 + ddH70Q4AAAA= + + True + + null + null + t53t29Y1A0abjVS1+xTo5g== + + t53t29Y1A0abjVS1+xTo5g== + 0 + + + + + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + + Notes + Notes + + + Password + 6f9J8T82DJ0= + + + Title + Sample Entry + + + URL + https://keepass.info/ + + + UserName + User Name + + + True + 0 + + Target Window + {USERNAME}{TAB}{PASSWORD}{TAB}{ENTER} + + + + + + YeputjepZky3vXID4/HHrA== + 0 + + + + + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + + Password + heKymtY= + + + Title + Sample Entry #2 + + + URL + https://keepass.info/help/kb/testform.html + + + UserName + Michael321 + + + True + 0 + + *Test Form - KeePass* + + + + + + + 3BxuF+oUekuMpLe67xmTLQ== + General + + 48 + + mNL70Q4AAAA= + mNL70Q4AAAA= + tNL70Q4AAAA= + PNH70Q4AAAA= + False + 2 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + vQsKl6dJiEiNvbTkjDVHcw== + Windows + + 38 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + 6bNmbKf/QUKN7I3GlNqqQw== + Network + + 3 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + Qz8rqQvYZUummUrakV6BFQ== + Internet + + 1 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + WMP+6xrurE+Jr6kFDwJtxQ== + eMail + + 19 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + mSY4yshaIU64BNreFBm+1w== + Homebanking + + 37 + + mNL70Q4AAAA= + mNL70Q4AAAA= + s9L70Q4AAAA= + PNH70Q4AAAA= + False + 1 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + + + diff --git a/test/src/test/resources/xml/V4-ChaCha20-AES.xml b/test/src/test/resources/xml/V4-ChaCha20-AES.xml new file mode 100644 index 00000000..1b687758 --- /dev/null +++ b/test/src/test/resources/xml/V4-ChaCha20-AES.xml @@ -0,0 +1,319 @@ + + + + KeePass + New V4 Database + mNL70Q4AAAA= + This is a new database + mNL70Q4AAAA= + + ddH70Q4AAAA= + 365 + + ddH70Q4AAAA= + -1 + -1 + + False + False + True + False + False + + + True + AAAAAAAAAAAAAAAAAAAAAA== + ddH70Q4AAAA= + AAAAAAAAAAAAAAAAAAAAAA== + ddH70Q4AAAA= + 3mzTUVoVAkO7DFaZ6Soxrw== + 3mzTUVoVAkO7DFaZ6Soxrw== + 10 + 6291456 + E6Xp2w4AAAA= + + + + 3mzTUVoVAkO7DFaZ6Soxrw== + NewV4Database + + 49 + + ddH70Q4AAAA= + ddH70Q4AAAA= + tdL70Q4AAAA= + PNH70Q4AAAA= + False + 4 + ddH70Q4AAAA= + + True + + null + null + t53t29Y1A0abjVS1+xTo5g== + + t53t29Y1A0abjVS1+xTo5g== + 0 + + + + + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + + Notes + Notes + + + Password + QwJNlVEnjJU= + + + Title + Sample Entry + + + URL + https://keepass.info/ + + + UserName + User Name + + + True + 0 + + + Target Window + {USERNAME}{TAB}{PASSWORD}{TAB}{ENTER} + + + + + + YeputjepZky3vXID4/HHrA== + 0 + + + + + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + + Notes + + + + Password + d0Izuk4= + + + Title + Sample Entry #2 + + + URL + https://keepass.info/help/kb/testform.html + + + UserName + Michael321 + + + True + 0 + + + *Test Form - KeePass* + + + + + + + TyxuU/7td0iU8+b0iHRh3w== + 0 + + + + + + mNL70Q4AAAA= + OiL90Q4AAAA= + jCL90Q4AAAA= + PNH70Q4AAAA= + False + 1 + OiL90Q4AAAA= + + + Notes + + + + Password + cwPX20A= + + + Title + Sample Entry #2 - Copy + + + URL + https://keepass.info/help/kb/testform.html + + + UserName + Michael321 + + + True + 0 + + + *Test Form - KeePass* + + + + + + + 3BxuF+oUekuMpLe67xmTLQ== + General + + 48 + + mNL70Q4AAAA= + mNL70Q4AAAA= + tNL70Q4AAAA= + PNH70Q4AAAA= + False + 2 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + vQsKl6dJiEiNvbTkjDVHcw== + Windows + + 38 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + 6bNmbKf/QUKN7I3GlNqqQw== + Network + + 3 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + Qz8rqQvYZUummUrakV6BFQ== + Internet + + 1 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + WMP+6xrurE+Jr6kFDwJtxQ== + eMail + + 19 + + mNL70Q4AAAA= + mNL70Q4AAAA= + mNL70Q4AAAA= + PNH70Q4AAAA= + False + 0 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + mSY4yshaIU64BNreFBm+1w== + Homebanking + + 37 + + mNL70Q4AAAA= + mNL70Q4AAAA= + s9L70Q4AAAA= + PNH70Q4AAAA= + False + 1 + mNL70Q4AAAA= + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + + + + diff --git a/test/src/test/resources/xml/V4-ChaCha20-Argon2-Attachment.xml b/test/src/test/resources/xml/V4-ChaCha20-Argon2-Attachment.xml new file mode 100644 index 00000000..1a6574c2 --- /dev/null +++ b/test/src/test/resources/xml/V4-ChaCha20-Argon2-Attachment.xml @@ -0,0 +1,154 @@ + + + + KeePass + BoES0g4AAAA= + + qZ1Tzw4AAAA= + + qZ1Tzw4AAAA= + + qZ1Tzw4AAAA= + 365 + + tp1Tzw4AAAA= + -1 + -1 + + False + False + True + False + False + + True + AAAAAAAAAAAAAAAAAAAAAA== + qZ1Tzw4AAAA= + AAAAAAAAAAAAAAAAAAAAAA== + qZ1Tzw4AAAA= + 10 + 6291456 + TOEWAvT9GlIjOkSMv9F+aw== + TOEWAvT9GlIjOkSMv9F+aw== + + + + + TOEWAvT9GlIjOkSMv9F+aw== + Root + + 48 + + qZ1Tzw4AAAA= + qZ1Tzw4AAAA= + qZ1Tzw4AAAA= + qZ1Tzw4AAAA= + False + 0 + qZ1Tzw4AAAA= + + True + + null + null + GPv+n0B4upj0sZd+nuuiBQ== + + GPv+n0B4upj0sZd+nuuiBQ== + 0 + + + + + + wZ1Tzw4AAAA= + J55Tzw4AAAA= + J55Tzw4AAAA= + wZ1Tzw4AAAA= + False + 0 + J55Tzw4AAAA= + + + Notes + + + + Password + + + + Title + Test attachment + + + URL + + + + UserName + + + + letter J.jpeg + + + + True + 0 + + + + + mwq3nFNpPrvjXlsWy/6FTQ== + 0 + + + + + + 2KJTzw4AAAA= + DKNTzw4AAAA= + DKNTzw4AAAA= + 2KJTzw4AAAA= + False + 0 + DKNTzw4AAAA= + + + Notes + + + + Password + + + + Title + Test 2 Attachments + + + URL + + + + UserName + + + + letter J.jpeg + + + + letter L.jpeg + + + + True + 0 + + + + + + + \ No newline at end of file diff --git a/util/pom.xml b/util/pom.xml new file mode 100644 index 00000000..c3d3fb2d --- /dev/null +++ b/util/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + org.linguafranca.pwdb + KeePassJava2-parent + 2.2.4 + + + PWDB :: Util + Some underlying utilities which are needed for running tests. + util + + + true + + + + + junit + junit + + + + \ No newline at end of file diff --git a/util/src/main/java/org/linguafranca/test/maven/MavenRunListener.java b/util/src/main/java/org/linguafranca/test/maven/MavenRunListener.java new file mode 100644 index 00000000..9881b221 --- /dev/null +++ b/util/src/main/java/org/linguafranca/test/maven/MavenRunListener.java @@ -0,0 +1,55 @@ +package org.linguafranca.test.maven; + +import org.junit.runner.Description; +import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; + +/** + * Helper class to identify the test being run by maven surefire in the log output. This + * is particularly useful if there is a lot of "chatty" testing going on. + *

+ * See: Using Custom Listeners and Reporters + */ +@RunListener.ThreadSafe +public class MavenRunListener extends RunListener { + static Logger logger = LoggerFactory.getLogger("MavenRunListener"); + static Marker marker = MarkerFactory.getMarker("Starting Test"); + static Marker endMarker = MarkerFactory.getMarker("Finished Test"); + static Marker failMarker = MarkerFactory.getMarker("Failed Test"); + static Marker assumeFailMarker = MarkerFactory.getMarker("Assumption Failed"); + static Marker ignoredMarker = MarkerFactory.getMarker("Ignored Test"); + + @Override + public void testStarted(Description description) throws Exception { + super.testStarted(description); + logger.info(marker, "{}", description.getDisplayName()); + } + + @Override + public void testFailure(Failure failure) throws Exception { + super.testFailure(failure); + logger.error(failMarker, "{} {}", failure.getMessage(), failure.getDescription().getDisplayName()); + } + + @Override + public void testAssumptionFailure(Failure failure) { + super.testAssumptionFailure(failure); + logger.warn(assumeFailMarker, "{} {}", failure.getMessage(), failure.getDescription().getDisplayName()); + } + + @Override + public void testIgnored(Description description) throws Exception { + super.testIgnored(description); + logger.info(ignoredMarker, "{}", description.getDisplayName()); + } + + @Override + public void testFinished(Description description) throws Exception { + super.testFinished(description); + logger.info(endMarker, "Finished {}", description.getDisplayName()); + } +} diff --git a/util/src/main/java/org/linguafranca/test/util/HexViewer.java b/util/src/main/java/org/linguafranca/test/util/HexViewer.java new file mode 100644 index 00000000..f23538aa --- /dev/null +++ b/util/src/main/java/org/linguafranca/test/util/HexViewer.java @@ -0,0 +1,33 @@ +package org.linguafranca.test.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; + +import static org.linguafranca.test.util.TestUtil.getTestPrintStream; + +/** + * Useful for e.g. viewing the raw file contents + */ +public class HexViewer { + + static PrintStream printStream = getTestPrintStream(); + + public static void list(InputStream is) throws IOException { + for (int i = 0; i < 32; i++) { + byte[] buf = new byte [16]; + is.read(buf); + StringBuilder sb = new StringBuilder(); + for (byte b: buf) { + sb.append(String.format("%02X ", b)); + } + sb.append(" "); + for (byte b : buf) { + sb.append(b < 0x20 || b > 0x7e ? (char) 0x00B7 : (char) b); + } + printStream.println(sb); + } + } +} + + diff --git a/util/src/main/java/org/linguafranca/test/util/TestUtil.java b/util/src/main/java/org/linguafranca/test/util/TestUtil.java new file mode 100644 index 00000000..7476ddc4 --- /dev/null +++ b/util/src/main/java/org/linguafranca/test/util/TestUtil.java @@ -0,0 +1,27 @@ +package org.linguafranca.test.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; + +public class TestUtil { + /** + * Do nothing output stream + */ + public static class NullOutputStream extends OutputStream { + + @Override + public void write(int b) throws IOException { + + } + } + /** + * set system property to suppress output from tests + * @return if "inhibitConsoleOutput" has been set, e.g. in a profile + */ + public static PrintStream getTestPrintStream() { + return Boolean.getBoolean("inhibitConsoleOutput") ? + new PrintStream(new NullOutputStream()) : + new PrintStream(System.out); + } +}