diff --git a/.circleci/config.yml b/.circleci/config.yml
index 5151a953..1890bf8f 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -6,26 +6,6 @@ version: 2.1
# 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/
@@ -51,8 +31,7 @@ jobs:
# 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.
+ build-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/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..721db89e
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,23 @@
+# Editor configuration, see https://editorconfig.org
+root = true
+
+[*.java]
+charset = utf-8
+indent_style = space
+indent_size = 4
+# DO NOT USE insert_final_newline = true
+#insert_final_newline = true
+
+[*.yaml]
+indent_size = 2
+indent_style = space
+
+[*.sh]
+end_of_line = lf
+
+[*.cmd]
+end_of_line = crlf
+
+[*.py]
+indent_size = 4
+indent_style = space
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..1ddff4b5
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,39 @@
+* -text
+
+*.css text
+*.scss text
+*.sass text
+*.html text
+*.java text
+*.js text
+*.json text
+*.properties text
+*.txt text
+*.xml text
+*.py text
+*.cmd text
+*.bat text
+*.yaml text
+*.yml text
+*.sql text
+# *.ts text
+
+.gitignore text
+.gitattributes text
+.slugignore text
+.editorconfig text
+
+COPYING text
+README.md text
+mvnw text
+
+
+# These files are binary and should be left untouched
+*.class binary
+*.jar binary
+*.gif binary
+*.jpg binary
+*.png binary
+*.ico binary
+*.mp4 binary
+*.xcf binary
diff --git a/.gitignore b/.gitignore
index 3ee52c53..3da9b48e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ build
.vscode/*
*.versionsBackup
**/target
+.idea
#settings.gradle
#gradlew
@@ -10,4 +11,5 @@ build
#.gradle
#gradle
#build.gradle
+Calc*_V3.class
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 3d066ab9..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-language: java
-jdk: openjdk7
-script: mvn clean test
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c7a0aead..c6df680d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,39 @@
Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.com) and [Semantic Versioning](http://semver.org/spec/v2.0.0.html)
-## [2.2.3-SNAPSHOT]
+## [3.0.0-SNAPSHOT] (ongoing)
+
+### Changed
+
+- [Issue 81, 83] Remove junit dependency from main code and resolve test failure from UTF-8 encoding in test resources
+- [Issue 87] Problem with incorrect serialization of CustomIcons in Jackson implementation
+- [Issue 88] Incompatibility to KeePass due t missing empty element in autotype field in Jackson implementation
+- [Issue 90] Update dependencies to resolve security vulnerabilities
+- update to Java 11
+- refactor API
+ - remove complicated generics
+ - fluent element builder
+ - additional convenience methods
+- refactor examples
+- refactor modules
+ - rename Jackson database to KDBX database
+ - add experimental `basic` database support
+ - remove Simple database
+ - remove JAXB database
+ - remove DOM database
+ - remove `test` and `util` modules
+- refactor tests
+ - restructure
+ - create a test-jar for dependencies
+ - "upgrade" to JUnit 5
+
+
+## [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
@@ -13,6 +45,12 @@ Trying to follow the suggestions at [Keep a Change Log](http://keepachangelog.co
- Jackson implementation supports this interface
- Other implementations throw exceptions appropriately as unsupported
+- [Issue #70] Improved support for KDBX 4.1 format (Jackson only)
+- [ISSUE #71] Module restructure to avoid test files, junit being pulled in unnecessarily
+
+### Fixed
+
+KDBX file format version 4.1 now supported (Issue-70) (in Jackson, not Simple)
## [2.2.2] 2024-09-06
diff --git a/DatabaseStructure.svg b/DatabaseStructure.svg
index 0694e7aa..6e7d6b57 100644
--- a/DatabaseStructure.svg
+++ b/DatabaseStructure.svg
@@ -1,4 +1,21 @@
+
+
\ No newline at end of file
diff --git a/KdbxDiagram.svg b/KdbxDiagram.svg
index fb278b1e..6aaefa8e 100644
--- a/KdbxDiagram.svg
+++ b/KdbxDiagram.svg
@@ -1,4 +1,21 @@
+
+
\ No newline at end of file
diff --git a/ModuleStructure.svg b/ModuleStructure.svg
index 9579dffe..d55374a0 100644
--- a/ModuleStructure.svg
+++ b/ModuleStructure.svg
@@ -1,4 +1,21 @@
+
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/PropertyValueProtection.md b/PropertyValueProtection.md
index ccb6f87d..f0e12a05 100644
--- a/PropertyValueProtection.md
+++ b/PropertyValueProtection.md
@@ -8,7 +8,7 @@ that have sensitive values are called "Protected Properties".
KDBX files contain a list of the standard properties and whether they are to be treated as protected
by default. The standard properties include Password, which is so treated. There doesn't seem to be a way of updating
this list in the Windows KeePass implementation, and in any case documentation says that the
-list is updated to default after load of a database, so it seems that it ignored.
+list is updated to default after load of a database, so it seems that it is ignored.
Individual properties can be marked as "protect in memory", but there is no way to indicate that
all properties of that name should be protected.
@@ -63,7 +63,7 @@ luck trying to avoid strings at all. Likewise, if you collect passwords from a d
After the deserialization process, the passwords are stored in RAM. Therefore:
**Hashing the password is not feasible**, as the hash function is a one-way process.
-This means that once a password is hashed, it cannot be reverted back to its original form,
+This means that once a password is hashed, it cannot be reverted to its original form,
which makes hashing unsuitable in this context. Since we need to retrieve and view
the saved passwords (as per the requirements), hashing does not meet the objective.
@@ -71,9 +71,10 @@ the saved passwords (as per the requirements), hashing does not meet the objecti
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.
+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.
diff --git a/KDBX.3.1.xsd b/XSD/KDBX.3.1.xsd
similarity index 99%
rename from KDBX.3.1.xsd
rename to XSD/KDBX.3.1.xsd
index ff22a5c7..be5c71f8 100644
--- a/KDBX.3.1.xsd
+++ b/XSD/KDBX.3.1.xsd
@@ -1,18 +1,19 @@
diff --git a/XSD/KDBX.4.1.reichl.xsd b/XSD/KDBX.4.1.reichl.xsd
new file mode 100644
index 00000000..6ccdda40
--- /dev/null
+++ b/XSD/KDBX.4.1.reichl.xsd
@@ -0,0 +1,509 @@
+
+
+
+
+
+ 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/XSD/KDBX.4.1.xsd b/XSD/KDBX.4.1.xsd
new file mode 100644
index 00000000..c6b3058a
--- /dev/null
+++ b/XSD/KDBX.4.1.xsd
@@ -0,0 +1,584 @@
+
+
+
+
+
+ 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/XSD/KDBX.4.xsd
similarity index 99%
rename from KDBX.4.xsd
rename to XSD/KDBX.4.xsd
index 4fb7d70a..d0b7b130 100644
--- a/KDBX.4.xsd
+++ b/XSD/KDBX.4.xsd
@@ -1,18 +1,19 @@
- KeePassJava2-parent
- org.linguafranca.pwdb
- 2.2.3-SNAPSHOT
+ KeePassJava2.parent
+ mh.keepass
+ 1.0.0.1../pom.xml
@@ -27,32 +28,17 @@
KeePassJava2KeePassJava2 :: All
- KDB Database support as well as KDBX 3.1: DOM, JAXB and Simple implementations.
+ Composite containing KDBX and KDB Database support
- org.linguafranca.pwdb
- KeePassJava2-kdb
- ${project.version}
-
-
- org.linguafranca.pwdb
- KeePassJava2-dom
- ${project.version}
-
-
- org.linguafranca.pwdb
- KeePassJava2-jaxb
- ${project.version}
-
-
- org.linguafranca.pwdb
- KeePassJava2-simple
+ mh.keepass
+ KeePassJava2.kdb${project.version}
- org.linguafranca.pwdb
- KeePassJava2-jackson
+ mh.keepass
+ KeePassJava2.kdbx.database${project.version}
diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/base/AbstractJaxbParentedBinding.java b/all/src/main/java/org/linguafranca/all/IgnoreMe.java
similarity index 56%
rename from jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/base/AbstractJaxbParentedBinding.java
rename to all/src/main/java/org/linguafranca/all/IgnoreMe.java
index f9cb3ae1..d2d2bb71 100644
--- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/base/AbstractJaxbParentedBinding.java
+++ b/all/src/main/java/org/linguafranca/all/IgnoreMe.java
@@ -1,30 +1,24 @@
/*
- * Copyright 2015 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.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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.base;
-
-import javax.xml.bind.annotation.XmlTransient;
+package org.linguafranca.all;
/**
- * Ancestor class for Entry and Group Jaxb Bindings, allowing insertion of
- * a reference to the parent group.
- *
- * @author jo
+ * This is here for Javadoc generation purposes only
*/
-public abstract class AbstractJaxbParentedBinding {
- @XmlTransient
- public AbstractJaxbParentedBinding parent;
+public class IgnoreMe {
}
diff --git a/all/src/main/java/org/linguafranca/all/package-info.java b/all/src/main/java/org/linguafranca/all/package-info.java
new file mode 100644
index 00000000..83edcabd
--- /dev/null
+++ b/all/src/main/java/org/linguafranca/all/package-info.java
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ * 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.
+ *
+ */
+
+/**
+ * This package/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.all;
\ 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
deleted file mode 100644
index 0f2521c5..00000000
--- a/all/src/main/java/org/linguafranca/pwdb/IgnoreMe.java
+++ /dev/null
@@ -1,7 +0,0 @@
-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
deleted file mode 100644
index a544df96..00000000
--- a/all/src/main/java/org/linguafranca/pwdb/package-info.java
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * 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/basic/pom.xml b/basic/pom.xml
new file mode 100644
index 00000000..87186e99
--- /dev/null
+++ b/basic/pom.xml
@@ -0,0 +1,72 @@
+
+
+
+
+ 4.0.0
+
+ mh.keepass
+ KeePassJava2.parent
+ 1.0.0.1
+
+
+ basic
+ PWDB :: Basic
+ Experimental database implementation to explore serialization.
+
+ true
+
+
+
+
+ mh.keepass
+ database
+ ${project.version}
+ compile
+
+
+ mh.keepass
+ KeePassJava2.kdbx.io
+ 1.0.0.1
+ compile
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-xml
+ 2.21.2
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ 2.21.2
+
+
+ com.fasterxml.woodstox
+ woodstox-core
+ 7.1.0
+
+
+ mh.keepass
+ database
+ ${project.version}
+ test-jar
+ test
+
+
+
\ No newline at end of file
diff --git a/basic/src/main/java/org/linguafranca/pwdb/basic/BasicDatabase.java b/basic/src/main/java/org/linguafranca/pwdb/basic/BasicDatabase.java
new file mode 100644
index 00000000..7abd4f4f
--- /dev/null
+++ b/basic/src/main/java/org/linguafranca/pwdb/basic/BasicDatabase.java
@@ -0,0 +1,146 @@
+/*
+ * 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.
+ * 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.basic;
+
+import org.jspecify.annotations.Nullable;
+import org.linguafranca.pwdb.*;
+import org.linguafranca.pwdb.format.KdbxHeader;
+import org.linguafranca.pwdb.format.KdbxSerializer;
+import org.linguafranca.pwdb.format.KdbxStreamFormat;
+import org.linguafranca.pwdb.protect.ProtectedDatabase;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.time.Instant;
+import java.util.Objects;
+
+/**
+ * A basic implementation of a Database for experimentation and testing
+ */
+public class BasicDatabase extends ProtectedDatabase {
+ private final BasicGroup root = new BasicGroup(this, "Root");
+ private String databaseName = "New Database";
+ private String databaseDescription = "Database created on " + Instant.now().toString();
+ private StreamFormat> streamFormat;
+
+ @Override
+ public Group getRootGroup() {
+ return root;
+ }
+
+ @Override
+ public Group newGroup() {
+ return new BasicGroup(this);
+ }
+
+ @Override
+ public Entry newEntry() {
+ return new BasicEntry(this);
+ }
+
+ @Override
+ public Icon newIcon() {
+ return new BasicIcon();
+ }
+
+ @Override
+ public Icon newIcon(Integer i) {
+ return new BasicIcon(i);
+ }
+
+ @Override
+ public boolean isRecycleBinEnabled() {
+ return false;
+ }
+
+ @Override
+ public void enableRecycleBin(boolean enable) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public @Nullable Group getRecycleBin() {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return databaseName;
+ }
+
+ @Override
+ public void setName(String name) {
+ this.databaseName = name;
+ }
+
+ @Override
+ public String getDescription() {
+ return databaseDescription;
+ }
+
+ @Override
+ public void setDescription(String description) {
+ this.databaseDescription = description;
+ }
+
+ @Override
+ public void save(Credentials credentials, OutputStream outputStream) throws IOException {
+ if (Objects.isNull(streamFormat)) {
+ streamFormat = new KdbxStreamFormat(new KdbxHeader(4));
+ }
+ save(streamFormat, credentials, outputStream);
+ }
+
+ @Override
+ public void save(StreamFormat streamFormat,
+ Credentials credentials,
+ OutputStream outputStream) throws IOException {
+ KdbxStreamFormat kdbxStreamFormat = (KdbxStreamFormat) streamFormat;
+ KdbxHeader header = kdbxStreamFormat.getStreamConfiguration();
+ OutputStream encryptedOutputStream = KdbxSerializer.createEncryptedOutputStream(credentials, header, outputStream);
+ BasicDatabaseSerializer bds = new BasicDatabaseSerializer.Xml(header.getInnerStreamEncryptor());
+ bds.save(this, encryptedOutputStream);
+ }
+
+ @Override
+ public StreamFormat> getStreamFormat() {
+ return streamFormat;
+ }
+
+
+ /**
+ * On load add parents
+ *
+ * @param parent a parent to recurse
+ */
+ void fixUp(BasicGroup parent) {
+ parent.database = this;
+ for (Group group : parent.getGroups()) {
+ BasicGroup basicGroup = (BasicGroup) group;
+ basicGroup.parent = parent;
+ fixUp(basicGroup);
+ }
+
+ for (Entry entry : parent.getEntries()) {
+ BasicEntry basicEntry = (BasicEntry) entry;
+ basicEntry.database = this;
+ basicEntry.parent = parent;
+ }
+ }
+
+}
diff --git a/basic/src/main/java/org/linguafranca/pwdb/basic/BasicDatabaseSerializer.java b/basic/src/main/java/org/linguafranca/pwdb/basic/BasicDatabaseSerializer.java
new file mode 100644
index 00000000..c44425f1
--- /dev/null
+++ b/basic/src/main/java/org/linguafranca/pwdb/basic/BasicDatabaseSerializer.java
@@ -0,0 +1,312 @@
+/*
+ * 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.
+ * 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.basic;
+
+import com.fasterxml.jackson.annotation.*;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.linguafranca.pwdb.PropertyValue;
+import org.linguafranca.pwdb.StreamFormat;
+import org.linguafranca.pwdb.protect.ProtectedDatabase;
+import org.linguafranca.pwdb.security.StreamEncryptor;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+/**
+ * This class is responsible for serializing and deserializing a BasicDatabase to and from a stream.
+ */
+public interface BasicDatabaseSerializer {
+
+ /**
+ * Serialize a BasicDatabase to a stream
+ *
+ * @param database the database to serialize
+ * @param outputStream the stream to serialize to
+ */
+ void save(BasicDatabase database, OutputStream outputStream) throws IOException;
+
+ /**
+ * Deserialize a BasicDatabase from a stream
+ *
+ * @param inputStream the serialized form of the database
+ * @return the deserialized database
+ */
+ BasicDatabase load(InputStream inputStream) throws IOException;
+
+ /**
+ * Serialize a BasicDatabase to a stream, with a runtime exception thrown if an IOException occurs
+ */
+ default void saveNx(BasicDatabase database, OutputStream outputStream){
+ try {
+ save(database, outputStream);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Deserialize a BasicDatabase from a stream, with a runtime exception thrown if an IOException occurs
+ */
+ default BasicDatabase loadNx(InputStream inputStream){
+ try {
+ return load(inputStream);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ /**
+ * XML implementation of containing BasicDatabaseSerializer
+ */
+ class Xml implements BasicDatabaseSerializer {
+ private final ObjectMapper objectMapper;
+ private final StreamEncryptor streamEncryptor;
+
+ public Xml() {
+ this(new StreamEncryptor.None());
+ }
+
+ public Xml(StreamEncryptor streamEncryptor) {
+ this.streamEncryptor = streamEncryptor;
+ this.objectMapper = init();
+ }
+
+ @Override
+ public void save(BasicDatabase database, OutputStream outputStream) throws IOException {
+ objectMapper.writer().withRootName("database").writeValue(outputStream, database);
+ }
+
+ @Override
+ public BasicDatabase load(InputStream inputStream) throws IOException {
+ BasicDatabase database2 = objectMapper.readValue(inputStream, BasicDatabase.class);
+ database2.fixUp((BasicGroup) database2.getRootGroup());
+ return database2;
+ }
+
+ private ObjectMapper init() {
+ SimpleModule module = new SimpleModule()
+ .addDeserializer(PropertyValue.class, new PropertyValueDeserializer(streamEncryptor))
+ .addSerializer(PropertyValue.class, new PropertyValueSerializer(streamEncryptor))
+ .addDeserializer(BasicIcon.class, new IconDeserializer())
+ .addSerializer(BasicIcon.class, new IconSerializer());
+ XmlMapper mapper = XmlMapper.builder()
+ // ignore everything except fields
+ .visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
+ .visibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE)
+ .visibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE)
+ .visibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.NONE)
+ .visibility(PropertyAccessor.CREATOR, JsonAutoDetect.Visibility.NONE)
+ .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
+ // even though the source file is not annotated, we want annotations in the Configuration.class
+ .enable(MapperFeature.USE_ANNOTATIONS)
+ // add mixin to main classes
+ .addMixIn(BasicGroup.class, Configuration.class)
+ .addMixIn(BasicEntry.class, Configuration.class)
+ .addMixIn(BasicDatabase.class, Configuration.class)
+ .build();
+
+
+ mapper
+ // for PropertyValue serialization
+ .registerModule(module)
+ // for Instant processing
+ .registerModule(new JavaTimeModule())
+ // pretty print
+ .enable(SerializationFeature.INDENT_OUTPUT)
+ // suppress empty fields
+ .setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
+
+ return mapper;
+ }
+
+ /**
+ * Configure special treatment on serialization of properties
+ */
+ public static class PropertyValueSerializer extends JsonSerializer {
+ StreamEncryptor streamEncryptor;
+
+ public PropertyValueSerializer() {
+ this(new StreamEncryptor.None());
+ }
+
+ public PropertyValueSerializer(StreamEncryptor streamEncryptor) {
+ this.streamEncryptor = streamEncryptor;
+ }
+
+ /**
+ * Don't serialize empty properties
+ *
+ * TODO: at present, policy is to have all 5 standard properties present with empty
+ * value if necessary. Review policy. Deserialization messes up constructor
+ * behaviour which is to initialise those properties that way.
+ */
+ @Override
+ public boolean isEmpty(SerializerProvider provider, PropertyValue value) {
+ // TODO: return (value.getValueAsString().isEmpty());
+ return false;
+ }
+
+ /**
+ * write protected properties with a flag to show they need to be protected on input
+ */
+ @Override
+ public void serialize(PropertyValue value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+
+ final ToXmlGenerator xmlGenerator = (ToXmlGenerator) gen;
+ // find out of we are serializing binaries
+ Object c = xmlGenerator.getOutputContext().getParent().getCurrentName();
+ boolean asBinary = c.equals("binaries");
+ xmlGenerator.writeStartObject();
+
+ if (value.isProtected()) {
+ xmlGenerator.setNextIsAttribute(true);
+ xmlGenerator.writeStringField("protected", "true");
+ asBinary = true; // write password as binary
+ }
+
+ if (asBinary) {
+ xmlGenerator.setNextIsAttribute(true);
+ xmlGenerator.writeStringField("binary", "true");
+ }
+ xmlGenerator.setNextIsAttribute(false);
+ xmlGenerator.setNextIsUnwrapped(true);
+
+ if (asBinary) {
+ byte[] bytes = value.getValueAsBytes();
+ if (value.isProtected()) {
+ bytes = streamEncryptor.encrypt(bytes);
+ }
+ xmlGenerator.writeBinaryField("text", bytes);
+ } else {
+ xmlGenerator.writeStringField("text", value.getValueAsString());
+ }
+ xmlGenerator.writeEndObject();
+ }
+ }
+
+ /**
+ * Property deserialization
+ */
+ public static class PropertyValueDeserializer extends JsonDeserializer {
+ PropertyValue.Strategy strategy;
+ StreamEncryptor streamEncryptor;
+
+ public PropertyValueDeserializer() {
+ this(new StreamEncryptor.None());
+ }
+
+ public PropertyValueDeserializer(StreamEncryptor streamEncryptor) {
+ this.streamEncryptor = streamEncryptor;
+ this.strategy = new PropertyValue.Strategy.Default();
+ }
+
+ @Override
+ public PropertyValue deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
+ JsonNode node = jsonParser.getCodec().readTree(jsonParser);
+
+ if (node.isTextual()) {
+ return strategy.newUnprotected().of(node.textValue());
+ }
+ if (node.has("binary")) {
+ if (node.has("protected")) {
+ byte[] bytes = streamEncryptor.decrypt(node.get("").binaryValue());
+ return strategy.newProtected().of(bytes);
+ }
+ return strategy.newUnprotected().of(node.get("").binaryValue());
+ }
+ throw new IllegalStateException("Unknown property value format " + node);
+ }
+ }
+
+ /**
+ * Configure special treatment for serialization of icons
+ */
+ public static class IconSerializer extends JsonSerializer {
+ /**
+ * flatten icon representation
+ */
+ @Override
+ public void serialize(BasicIcon icon, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+
+ final ToXmlGenerator xmlGenerator = (ToXmlGenerator) gen;
+ xmlGenerator.writeStartObject();
+ xmlGenerator.setNextIsAttribute(true);
+ xmlGenerator.writeStringField("index", String.valueOf(icon.getIndex()));
+ xmlGenerator.writeEndObject();
+ }
+ }
+
+ /**
+ * Icon deserialization
+ */
+ public static class IconDeserializer extends JsonDeserializer {
+ PropertyValue.Strategy strategy = new PropertyValue.Strategy.Default();
+
+ @Override
+ public BasicIcon deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
+ JsonNode node = jsonParser.getCodec().readTree(jsonParser);
+
+ if (node.has("index")) {
+ return new BasicIcon(node.get("index").asInt());
+ }
+ throw new IllegalStateException("Unknown icon value format " + node);
+ }
+ }
+
+ /**
+ * Class is for binding Jackson annotations to a later Jackson "Mix-In"
+ */
+ public abstract static class Configuration {
+ // Tell Jackson how to treat lists of Entry and Groups
+ @JsonPropertyOrder({ "uuid", "icon", "properties", "binaries" })
+ protected static class BasicEntry {}
+
+ @JacksonXmlProperty(localName = "entry")
+ @JacksonXmlElementWrapper(useWrapping = false)
+ @JsonPropertyOrder({ "uuid", "icon", "properties", "binaries" })
+ protected List entries;
+
+ @JsonPropertyOrder({ "uuid", "name", "icon", "group", "entry" })
+ protected static class BasicGroup {}
+
+ @JacksonXmlProperty(localName = "group")
+ @JacksonXmlElementWrapper(useWrapping = false)
+ protected List groups;
+ // tell Jackson to ignore these fields
+ @JsonIgnore
+ BasicGroup parent;
+ @JsonIgnore
+ BasicDatabase database;
+ @JsonIgnore
+ ProtectedDatabase valueStrategy;
+ @JsonIgnore
+ boolean isDirty;
+ @JsonIgnore
+ StreamFormat> streamFormat;
+ }
+ }
+}
diff --git a/basic/src/main/java/org/linguafranca/pwdb/basic/BasicEntry.java b/basic/src/main/java/org/linguafranca/pwdb/basic/BasicEntry.java
new file mode 100644
index 00000000..adcbafdd
--- /dev/null
+++ b/basic/src/main/java/org/linguafranca/pwdb/basic/BasicEntry.java
@@ -0,0 +1,236 @@
+/*
+ * 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.
+ * 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.basic;
+
+import org.jspecify.annotations.Nullable;
+import org.linguafranca.pwdb.*;
+import org.linguafranca.pwdb.abstractdb.AbstractEntry;
+
+import java.time.Instant;
+import java.util.*;
+
+public class BasicEntry extends AbstractEntry {
+ protected BasicDatabase database;
+ protected BasicGroup parent;
+
+ private final UUID uuid;
+ private BasicIcon icon;
+ private final Map properties = new HashMap<>();
+ private final Map binaries = new HashMap<>();
+
+ private final Instant creationTime;
+ private @Nullable Instant accessTime;
+ private @Nullable Instant modifiedTime;
+ private Instant expiryTime;
+ private boolean isExpires;
+
+
+ public BasicEntry() {
+ this.uuid = UUID.randomUUID();
+ this.creationTime = Instant.now();
+ this.icon = new BasicIcon(1);
+ for (String name : Entry.STANDARD_PROPERTY_NAMES) {
+ properties.put(name, new PropertyValue.StringStore(""));
+ }
+ }
+
+ public BasicEntry(BasicDatabase basicDatabase) {
+ this();
+ this.database = basicDatabase;
+ }
+
+ @Override
+ protected void touch() {
+ database.setDirty(true);
+ }
+
+ @Override
+ public @Nullable String getProperty(String name) {
+ if (!STANDARD_PROPERTY_NAMES.contains(name) && !database.supportsNonStandardPropertyNames()) {
+ throw new IllegalArgumentException("Property " + name + " is not a standard property name");
+ }
+ if (!properties.containsKey(name)) {
+ return null;
+ }
+ updateAccessTime();
+ return properties.get(name).getValueAsString();
+ }
+
+ @Override
+ public BasicEntry setProperty(String name, String value) {
+ updateModifiedTime();
+ properties.put(name, PropertyValue.StringStore.getFactory().of(value));
+ return this;
+ }
+
+ @Override
+ public boolean removeProperty(String name) throws IllegalArgumentException, UnsupportedOperationException {
+ if (Entry.STANDARD_PROPERTY_NAMES.contains(name)) {
+ throw new IllegalArgumentException("Cannot remove standard property");
+ }
+ if (!database.supportsNonStandardPropertyNames()) {
+ throw new UnsupportedOperationException("Database does not support non-standard properties");
+ }
+ boolean exists = properties.containsKey(name);
+ if (exists) {
+ updateModifiedTime();
+ }
+ properties.remove(name);
+ return exists;
+ }
+
+ @Override
+ public List getPropertyNames() {
+ return new ArrayList<>(properties.keySet());
+ }
+
+ @Override
+ public PropertyValue getPropertyValue(String name) {
+ return properties.get(name);
+ }
+
+ @Override
+ public BasicEntry setPropertyValue(String name, PropertyValue value) {
+ properties.put(name, value);
+ return this;
+ }
+
+ public BasicEntry addProperty(String name, byte[] value){
+ properties.put(name, database.getPropertyValueStrategy().getFactoryFor(name).of(value));
+ return this;
+ }
+ public BasicEntry addProperty(String name, char[] value) {
+ properties.put(name, database.getPropertyValueStrategy().getFactoryFor(name).of(value));
+ return this;
+ }
+
+ public BasicEntry addProperty(String name, CharSequence value){
+ properties.put(name, database.getPropertyValueStrategy().getFactoryFor(name).of(value));
+ return this;
+ }
+ @Override
+ public byte@Nullable [] getBinaryProperty(String name) {
+ if (!binaries.containsKey(name)) {
+ return null;
+ }
+ updateAccessTime();
+ return binaries.get(name).getValueAsBytes();
+ }
+
+ @Override
+ public void setBinaryProperty(String name, byte[] value) {
+ updateModifiedTime();
+ binaries.put(name, PropertyValue.BytesStore.getFactory().of(value));
+ }
+
+ @Override
+ public boolean removeBinaryProperty(String name) throws UnsupportedOperationException {
+ boolean exists = binaries.containsKey(name);
+ if (exists) {updateModifiedTime();}
+ binaries.remove(name);
+ return exists;
+ }
+
+ @Override
+ public List getBinaryPropertyNames() {
+ return List.of(binaries.keySet().toArray(String[]::new));
+ }
+
+ @Override
+ public BasicDatabase getDatabase() {
+ return database;
+ }
+
+ @Override
+ public @Nullable BasicGroup getParent() {
+ return parent;
+ }
+
+ @Override
+ public UUID getUuid() {
+ return uuid;
+ }
+
+ @Override
+ public BasicIcon getIcon() {
+ return icon;
+ }
+
+ @Override
+ public void setIcon(Icon icon) {
+ this.icon = (BasicIcon) icon;
+ }
+
+ @Override
+ public Date getLastAccessTime() {
+ if (accessTime == null) {
+ return fromInstant(this.creationTime);
+ }
+ return fromInstant(this.accessTime);
+ }
+
+ @Override
+ public Date getCreationTime() {
+ return fromInstant(creationTime);
+ }
+
+ @Override
+ public boolean getExpires() {
+ return this.isExpires;
+ }
+
+ @Override
+ public void setExpires(boolean expires) {
+ this.isExpires = expires;
+ }
+
+ @Override
+ public Date getExpiryTime() {
+ return fromInstant(this.expiryTime);
+ }
+
+ @Override
+ public void setExpiryTime(Date expiryTime) throws IllegalArgumentException {
+ this.expiryTime = expiryTime.toInstant();
+ }
+
+ @Override
+ public Date getLastModificationTime() {
+ if (modifiedTime == null) {
+ return fromInstant(this.creationTime);
+ }
+ return fromInstant(this.modifiedTime);
+ }
+
+ private void updateAccessTime() {
+ accessTime = Instant.now();
+ touch();
+ }
+
+ private void updateModifiedTime() {
+ modifiedTime = Instant.now();
+ touch();
+ }
+
+ private static @Nullable Date fromInstant(@Nullable Instant instant) {
+ if (instant == null) {
+ return null;
+ }
+ return Date.from(instant);
+ }
+}
diff --git a/basic/src/main/java/org/linguafranca/pwdb/basic/BasicGroup.java b/basic/src/main/java/org/linguafranca/pwdb/basic/BasicGroup.java
new file mode 100644
index 00000000..adde7c00
--- /dev/null
+++ b/basic/src/main/java/org/linguafranca/pwdb/basic/BasicGroup.java
@@ -0,0 +1,161 @@
+/*
+ * 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.
+ * 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.basic;
+
+import org.jspecify.annotations.Nullable;
+import org.linguafranca.pwdb.Database;
+import org.linguafranca.pwdb.Entry;
+import org.linguafranca.pwdb.Group;
+import org.linguafranca.pwdb.Icon;
+import org.linguafranca.pwdb.abstractdb.AbstractGroup;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+public class BasicGroup extends AbstractGroup {
+ protected BasicGroup parent;
+ protected BasicDatabase database;
+ private final UUID uuid;
+ private String name;
+ private BasicIcon icon;
+
+ private final List groups = new ArrayList<>();
+ private final List entries = new ArrayList<>();
+
+
+
+ BasicGroup () {
+ this.database = null;
+ this.uuid = UUID.randomUUID();
+ this.icon = new BasicIcon();
+ this.name = "";
+ }
+ BasicGroup(BasicDatabase database) {
+ this();
+ this.database = database;
+ }
+
+ BasicGroup(BasicDatabase database, String name) {
+ this(database);
+ this.name = name;
+ }
+
+ @Override
+ public boolean isRootGroup() {
+ return database.getRootGroup().equals(this);
+ }
+
+ @Override
+ public boolean isRecycleBin() {
+ return false;
+ }
+
+ @Override
+ public @Nullable BasicGroup getParent() {
+ return parent;
+ }
+
+ @Override
+ public void setParent(Group parent) {
+ this.parent = (BasicGroup) parent;
+ }
+
+ @Override
+ public List getGroups() {
+ return new ArrayList<>(groups);
+ }
+
+ @Override
+ public int getGroupsCount() {
+ return groups.size();
+ }
+
+ @Override
+ public Group addGroup(Group group) {
+ BasicGroup basicGroup = (BasicGroup) group;
+ groups.add(basicGroup);
+ basicGroup.parent = this;
+ return group;
+ }
+
+ @Override
+ public Group removeGroup(Group group) {
+ BasicGroup basicGroup = (BasicGroup) group;
+ groups.remove(basicGroup);
+ basicGroup.parent = null;
+ return group;
+ }
+
+ @Override
+ public List getEntries() {
+ return Collections.unmodifiableList(entries);
+ }
+
+ @Override
+ public int getEntriesCount() {
+ return entries.size();
+ }
+
+ @Override
+ public BasicEntry addEntry(Entry entry) {
+ BasicEntry basicEntry = (BasicEntry) entry;
+ entries.add(basicEntry);
+ basicEntry.parent = this;
+ return basicEntry;
+ }
+
+ @Override
+ public BasicEntry removeEntry(Entry entry) {
+ BasicEntry basicEntry = (BasicEntry) entry;
+ entries.remove(basicEntry);
+ (basicEntry).parent = null;
+ return basicEntry;
+ }
+
+ @Override
+ public String getName() {
+ return this.name;
+ }
+
+ @Override
+ public void setName(String name) {
+ this.name=name;
+ }
+
+ @Override
+ public UUID getUuid() {
+ return this.uuid;
+ }
+
+ @Override
+ public Icon getIcon() {
+ return this.icon;
+ }
+
+ @Override
+ public void setIcon(Icon icon) {
+ this.icon = (BasicIcon) icon;
+ }
+
+ @Override
+ public Database getDatabase() {
+ return this.database;
+ }
+}
diff --git a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbIcon.java b/basic/src/main/java/org/linguafranca/pwdb/basic/BasicIcon.java
similarity index 62%
rename from jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbIcon.java
rename to basic/src/main/java/org/linguafranca/pwdb/basic/BasicIcon.java
index c558be3b..19006b47 100644
--- a/jaxb/src/main/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbIcon.java
+++ b/basic/src/main/java/org/linguafranca/pwdb/basic/BasicIcon.java
@@ -1,36 +1,34 @@
/*
- * Copyright 2015 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.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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;
+package org.linguafranca.pwdb.basic;
import org.linguafranca.pwdb.Icon;
-/**
- * @author jo
- */
-@SuppressWarnings("WeakerAccess")
-public class JaxbIcon implements Icon{
+import java.util.Objects;
+
+public class BasicIcon implements Icon {
private int index;
- public JaxbIcon(int iconID) {
- index = iconID;
+ public BasicIcon() {
}
- public JaxbIcon() {
- index = 0;
+ public BasicIcon(Integer i) {
+ this.index = i;
}
@Override
@@ -45,17 +43,13 @@ public void setIndex(int index) {
@Override
public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- JaxbIcon that = (JaxbIcon) o;
-
- return index == that.index;
-
+ if (!(o instanceof BasicIcon)) return false;
+ BasicIcon basicIcon = (BasicIcon) o;
+ return index == basicIcon.index;
}
@Override
public int hashCode() {
- return index;
+ return Objects.hashCode(index);
}
}
diff --git a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBasicDatabaseTest.java b/basic/src/main/java/org/linguafranca/pwdb/basic/package-info.java
similarity index 52%
rename from jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBasicDatabaseTest.java
rename to basic/src/main/java/org/linguafranca/pwdb/basic/package-info.java
index d4d08454..21f36f62 100644
--- a/jaxb/src/test/java/org/linguafranca/pwdb/kdbx/jaxb/JaxbBasicDatabaseTest.java
+++ b/basic/src/main/java/org/linguafranca/pwdb/basic/package-info.java
@@ -1,36 +1,28 @@
/*
- * Copyright 2015 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.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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.checks.BasicDatabaseChecks;
-import org.linguafranca.pwdb.Database;
-
-import java.io.IOException;
-
/**
- * @author jo
+ * This package contains the interfaces that define the org.linguafranca Database API.
+ *
+ * The API is designed to be a simple, low-level, read-write API for password databases.
+ *
+ * Class instances of the interfaces are contained in the sub-packages of this package.
*/
-public class JaxbBasicDatabaseTest extends BasicDatabaseChecks {
-
- public JaxbBasicDatabaseTest() throws IOException {
- }
+@NullMarked
+package org.linguafranca.pwdb.basic;
- @Override
- public Database createDatabase() {
- return new JaxbDatabase();
- }
-}
+import org.jspecify.annotations.NullMarked;
\ No newline at end of file
diff --git a/basic/src/test/java/org/linguafranca/pwdb/basic/BasicDatabaseKdbxTest.java b/basic/src/test/java/org/linguafranca/pwdb/basic/BasicDatabaseKdbxTest.java
new file mode 100644
index 00000000..47409b70
--- /dev/null
+++ b/basic/src/test/java/org/linguafranca/pwdb/basic/BasicDatabaseKdbxTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ * 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.basic;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Execution;
+import org.linguafranca.pwdb.Database;
+import org.linguafranca.pwdb.KdbConsts;
+import org.linguafranca.pwdb.format.KdbxCredentials;
+import org.linguafranca.pwdb.format.KdbxHeader;
+import org.linguafranca.pwdb.format.KdbxSerializer;
+import org.linguafranca.pwdb.format.KdbxStreamFormat;
+import org.linguafranca.pwdb.test.DatabaseTestBase;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.function.Supplier;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT;
+import static org.linguafranca.pwdb.Entry.STANDARD_PROPERTY_NAME.*;
+
+@Execution(CONCURRENT)
+public class BasicDatabaseKdbxTest {
+
+ private static final Supplier dbFunc = ()-> new DatabaseTestBase(BasicDatabase::new,
+ (_, inputStream) ->
+ new BasicDatabaseSerializer.Xml().loadNx(inputStream),
+ (database, _, outputStream) ->
+ new BasicDatabaseSerializer.Xml().saveNx((BasicDatabase) database, outputStream),
+ (_) -> null);
+
+ @Test
+ public void testKdbx(@TempDir Path tempPath) throws IOException {
+ DatabaseTestBase databaseTestBase = BasicDatabaseKdbxTest.dbFunc.get();
+ Database db = databaseTestBase.database;
+ db.setName("test");
+ db.getRootGroup().addEntry("test")
+ .addProperty(PASSWORD,"password")
+ .addProperty(URL,"http://example.com")
+ .addProperty(USER_NAME,"user");
+
+ Path dbPath = tempPath.resolve("test.kdbx");
+ db.save(new KdbxStreamFormat(), new KdbxCredentials(KdbConsts.PASSWORD_BYTES), Files.newOutputStream(dbPath));
+
+ KdbxHeader header = new KdbxHeader();
+ InputStream unencryptedInputStream = KdbxSerializer.createUnencryptedInputStream(new KdbxCredentials(KdbConsts.PASSWORD_BYTES),
+ header, Files.newInputStream(dbPath));
+
+ BasicDatabaseSerializer.Xml xml = new BasicDatabaseSerializer.Xml(header.getInnerStreamEncryptor());
+ Database db2 = xml.load(unencryptedInputStream);
+ assertEquals(db.getName(), db2.getName());
+ assertEquals(db2.getRootGroup().findEntries(e -> true, true).size(), db.getRootGroup().findEntries(e -> true, true).size());
+ assertEquals("password", db2.getRootGroup().findEntries(e -> e.getTitle().equals("test"), true).get(0).getPropertyValue(PASSWORD).getValueAsString());
+ }
+}
diff --git a/basic/src/test/java/org/linguafranca/pwdb/basic/BasicDatabaseSerializerTest.java b/basic/src/test/java/org/linguafranca/pwdb/basic/BasicDatabaseSerializerTest.java
new file mode 100644
index 00000000..5cfe5ca1
--- /dev/null
+++ b/basic/src/test/java/org/linguafranca/pwdb/basic/BasicDatabaseSerializerTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2025. Jo Rabin
+ * Copyright (c) 2026. Sergio Lissner
+ *
+ * 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.basic;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Execution;
+import org.linguafranca.pwdb.Entry;
+import org.linguafranca.pwdb.Group;
+import org.linguafranca.pwdb.Visitor;
+import org.linguafranca.pwdb.security.StreamEncryptor;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT;
+import static org.linguafranca.util.TestUtil.getTestPrintStream;
+
+@Execution(CONCURRENT)
+class BasicDatabaseSerializerTest {
+
+ static BasicDatabase setUpBasicDatabase() {
+
+ BasicDatabase database;
+ database = new BasicDatabase();
+ database.setName("My Database");
+ database.setDescription("A database of passwords");
+ database.getRootGroup().addGroup("Group 1")
+ .addEntry("Entry 1")
+ .addProperty(Entry.STANDARD_PROPERTY_NAME.USER_NAME, "John Doe")
+ .addProperty(Entry.STANDARD_PROPERTY_NAME.PASSWORD, "password123".getBytes())
+ .addEntry("Entry 2")
+ .addProperty(Entry.STANDARD_PROPERTY_NAME.USER_NAME, "Jane Doe")
+ .addProperty(Entry.STANDARD_PROPERTY_NAME.PASSWORD, "password123".getBytes())
+ .addProperty("non-standard", "non-standard value")
+ .addEntry("Entry 3")
+ .setBinaryProperty("gif", new byte[]{1, 2, 3, 4, 5});
+ return database;
+ }
+
+ public static Visitor setUpVisitor(BasicDatabase database, PrintStream out) {
+ Visitor visitor = new Visitor() {
+ @Override
+ public void startVisit(Group group) {
+ out.println("Group: " + group.getName() + " " + group.getUuid());
+ assertNotNull(database.findGroup(group.getUuid()));
+ assertNotNull(group.getDatabase());
+ if (!group.isRootGroup()) {
+ assertNotNull(group.getParent());
+ }
+ }
+
+ @Override
+ public void endVisit(Group group) {
+
+ }
+
+ @Override
+ public void visit(Entry entry) {
+ out.println("Entry: " + entry.getTitle() + " " + entry.getUuid());
+ for (String propertyName: entry.getPropertyNames()) {
+ out.println(" " + propertyName + ": " + entry.getPropertyValue(propertyName).getValueAsString());
+ }
+ for (String propertyName: entry.getBinaryPropertyNames()) {
+ out.println(" " + propertyName + ": " + Arrays.toString(entry.getBinaryProperty(propertyName)));
+ }
+ Entry entry1 = database.findEntry(entry.getUuid());
+ assertNotNull(entry1, "Entry not found " + entry.getUuid());
+ assertEquals(entry.getPropertyNames().size(), entry1.getPropertyNames().size());
+ assertNotNull(entry.getDatabase());
+ assertNotNull(entry.getParent());
+ }
+
+ @Override
+ public boolean isEntriesFirst() {
+ return false;
+ }
+ };
+ return visitor;
+ }
+
+ @Test
+ void saveAndLoad(@TempDir Path tempPath) throws IOException {
+ PrintStream out = getTestPrintStream();
+
+ BasicDatabase basicDatabase = setUpBasicDatabase();
+ OutputStream outputStream = Files.newOutputStream(tempPath.resolve("test.xml"));
+ new BasicDatabaseSerializer.Xml(new StreamEncryptor.Salsa20(new byte[0])).save(basicDatabase, outputStream);
+
+ InputStream inputStream = Files.newInputStream(tempPath.resolve( "test.xml"));
+ BasicDatabase loadedDatabase = new BasicDatabaseSerializer.Xml(new StreamEncryptor.Salsa20(new byte[0])).load(inputStream);
+ loadedDatabase.visit(setUpVisitor(basicDatabase, out));
+ out.println(loadedDatabase.getRootGroup().getDatabase().getName());
+ }
+}
\ No newline at end of file
diff --git a/basic/src/test/java/org/linguafranca/pwdb/basic/BasicDatabaseTest.java b/basic/src/test/java/org/linguafranca/pwdb/basic/BasicDatabaseTest.java
new file mode 100644
index 00000000..61c3ca01
--- /dev/null
+++ b/basic/src/test/java/org/linguafranca/pwdb/basic/BasicDatabaseTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ * 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.basic;
+
+import org.junit.jupiter.api.parallel.Execution;
+import org.linguafranca.pwdb.test.*;
+
+import java.util.function.Supplier;
+
+import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT;
+
+@Execution(CONCURRENT)
+public class BasicDatabaseTest
+ implements
+ TrivialDatabaseTest,
+ GroupsAndEntriesTest,
+ RecycleBinTest,
+ ProtectedPropertyTest,
+ ProtectedPropertyTest2 {
+
+ private static final Supplier dbFunc = ()-> new DatabaseTestBase(BasicDatabase::new,
+ (_, inputStream) ->
+ new BasicDatabaseSerializer.Xml().loadNx(inputStream),
+ (database, _, outputStream) ->
+ new BasicDatabaseSerializer.Xml().saveNx((BasicDatabase) database, outputStream),
+ (_) -> null);
+
+ @Override
+ public Supplier getDbFunc() {
+ return dbFunc;
+ }
+}
diff --git a/basic/src/test/java/org/linguafranca/pwdb/basic/BasicEntryTest.java b/basic/src/test/java/org/linguafranca/pwdb/basic/BasicEntryTest.java
new file mode 100644
index 00000000..d0b923cb
--- /dev/null
+++ b/basic/src/test/java/org/linguafranca/pwdb/basic/BasicEntryTest.java
@@ -0,0 +1,184 @@
+/*
+ * 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.
+ * 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.basic;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Execution;
+
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT;
+
+@Execution(CONCURRENT)
+class BasicEntryTest {
+
+ @Test
+ void testGetProperty() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+
+ entry.setProperty("name", "value");
+ assertEquals("value", entry.getProperty("name"));
+ }
+
+ @Test
+ void testSetProperty() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+
+ entry.setProperty("name", "value");
+ assertEquals("value", entry.getProperty("name"));
+ }
+
+ @Test
+ void testRemoveProperty() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+
+ entry.setProperty("name", "value");
+ assertTrue(entry.removeProperty("name"));
+ assertNull(entry.getProperty("name"));
+ }
+
+ @Test
+ void testGetPropertyNames() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+
+ entry.setProperty("name1", "value1");
+ entry.setProperty("name2", "value2");
+ List propertyNames = entry.getPropertyNames();
+ assertTrue(propertyNames.contains("name1"));
+ assertTrue(propertyNames.contains("name2"));
+ }
+
+ @Test
+ void testGetBinaryProperty() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+
+ byte[] data = {1, 2, 3};
+ entry.setBinaryProperty("binary", data);
+ assertArrayEquals(data, entry.getBinaryProperty("binary"));
+ }
+
+ @Test
+ void testSetBinaryProperty() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+
+ byte[] data = {1, 2, 3};
+ entry.setBinaryProperty("binary", data);
+ assertArrayEquals(data, entry.getBinaryProperty("binary"));
+ }
+
+ @Test
+ void testRemoveBinaryProperty() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+
+ byte[] data = {1, 2, 3};
+ entry.setBinaryProperty("binary", data);
+ assertTrue(entry.removeBinaryProperty("binary"));
+ assertNull(entry.getBinaryProperty("binary"));
+ }
+
+ @Test
+ void testGetBinaryPropertyNames() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+
+ byte[] data1 = {1, 2, 3};
+ byte[] data2 = {4, 5, 6};
+ entry.setBinaryProperty("binary1", data1);
+ entry.setBinaryProperty("binary2", data2);
+ List binaryPropertyNames = entry.getBinaryPropertyNames();
+ assertTrue(binaryPropertyNames.contains("binary1"));
+ assertTrue(binaryPropertyNames.contains("binary2"));
+ }
+
+ @Test
+ void testGetParent() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+ assertNull(entry.getParent());
+ }
+
+ @Test
+ void testGetUuid() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+ UUID uuid = entry.getUuid();
+ assertNotNull(uuid);
+ }
+
+ @Test
+ void testGetIcon() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+ assertNotNull(entry.getIcon());
+ }
+
+ @Test
+ void testSetIcon() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+ entry.setIcon(new BasicIcon(1) {});
+ assertEquals(1, entry.getIcon().getIndex());
+ }
+
+ @Test
+ void testGetLastAccessTime() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+ Date lastAccessTime = entry.getLastAccessTime();
+ assertNotNull(lastAccessTime);
+ }
+
+ @Test
+ void testGetCreationTime() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+ Date creationTime = entry.getCreationTime();
+ assertNotNull(creationTime);
+ }
+
+ @Test
+ void testGetExpires() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+ assertFalse(entry.getExpires());
+ }
+
+ @Test
+ void testSetExpires() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+ entry.setExpires(true);
+ assertTrue(entry.getExpires());
+ }
+
+ @Test
+ void testGetExpiryTime() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+ assertNull(entry.getExpiryTime());
+ }
+
+ @Test
+ void testSetExpiryTime() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+ Date expiryTime = Date.from(Instant.now());
+ entry.setExpiryTime(expiryTime);
+ assertEquals(expiryTime, entry.getExpiryTime());
+ }
+
+ @Test
+ void testGetLastModificationTime() {
+ BasicEntry entry= new BasicEntry(new BasicDatabase());
+ assertEquals(entry.getCreationTime(), entry.getLastModificationTime());
+ }
+}
diff --git a/basic/src/test/java/org/linguafranca/pwdb/basic/BasicGroupTest.java b/basic/src/test/java/org/linguafranca/pwdb/basic/BasicGroupTest.java
new file mode 100644
index 00000000..9882b169
--- /dev/null
+++ b/basic/src/test/java/org/linguafranca/pwdb/basic/BasicGroupTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.
+ * 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.basic;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Execution;
+import org.linguafranca.pwdb.Entry;
+import org.linguafranca.pwdb.Group;
+
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT;
+
+@Execution(CONCURRENT)
+class BasicGroupTest {
+
+ @Test
+ void testGetName() {
+ BasicGroup group = new BasicGroup(new BasicDatabase());
+ group.setName("Test Group");
+ assertEquals("Test Group", group.getName());
+ }
+
+ @Test
+ void testSetName() {
+ BasicGroup group = new BasicGroup(new BasicDatabase());
+ group.setName("Test Group");
+ assertEquals("Test Group", group.getName());
+ }
+
+ @Test
+ void testGetEntries() {
+ BasicGroup group = new BasicGroup(new BasicDatabase());
+ Entry entry = new BasicEntry(new BasicDatabase());
+ group.addEntry(entry);
+ List entries = group.getEntries();
+ assertTrue(entries.contains(entry));
+ }
+
+ @Test
+ void testAddEntry() {
+ BasicGroup group = new BasicGroup(new BasicDatabase());
+ Entry entry = new BasicEntry(new BasicDatabase());
+ group.addEntry(entry);
+ assertTrue(group.getEntries().contains(entry));
+ }
+
+ @Test
+ void testRemoveEntry() {
+ BasicGroup group = new BasicGroup(new BasicDatabase());
+ Entry entry = new BasicEntry(new BasicDatabase());
+ group.addEntry(entry);
+ group.removeEntry(entry);
+ assertFalse(group.getEntries().contains(entry));
+ }
+
+ @Test
+ void testGetGroups() {
+ BasicGroup group = new BasicGroup(new BasicDatabase());
+ Group subGroup = new BasicGroup(new BasicDatabase());
+ group.addGroup(subGroup);
+ List groups = group.getGroups();
+ assertTrue(groups.contains(subGroup));
+ }
+
+ @Test
+ void testAddGroup() {
+ BasicGroup group = new BasicGroup(new BasicDatabase());
+ Group subGroup = new BasicGroup(new BasicDatabase());
+ group.addGroup(subGroup);
+ assertTrue(group.getGroups().contains(subGroup));
+ }
+
+ @Test
+ void testRemoveGroup() {
+ BasicGroup group = new BasicGroup(new BasicDatabase());
+ Group subGroup = new BasicGroup(new BasicDatabase());
+ group.addGroup(subGroup);
+ assertTrue(group.getGroups().contains(subGroup));
+ assertEquals(1, group.getGroups().size());
+ assertEquals(group, subGroup.getParent());
+ group.removeGroup(subGroup);
+ assertEquals(0, group.getGroups().size());
+ assertNull(subGroup.getParent());
+ assertFalse(group.getGroups().contains(subGroup));
+ }
+
+ @Test
+ void testGetParent() {
+ BasicGroup group = new BasicGroup(new BasicDatabase());
+ assertNull(group.getParent());
+ }
+
+ @Test
+ void testGetUuid() {
+ BasicGroup group = new BasicGroup(new BasicDatabase());
+ UUID uuid = group.getUuid();
+ assertNotNull(uuid);
+ }
+
+ @Test
+ void testGetIcon() {
+ BasicGroup group = new BasicGroup(new BasicDatabase());
+ assertEquals(0, group.getIcon().getIndex());
+ }
+
+ @Test
+ void testSetIcon() {
+ BasicGroup group = new BasicGroup(new BasicDatabase());
+ group.setIcon(new BasicIcon(1));
+ assertEquals(1, group.getIcon().getIndex());
+ }
+}
\ No newline at end of file
diff --git a/basic/src/test/java/org/linguafranca/pwdb/basic/package-info.java b/basic/src/test/java/org/linguafranca/pwdb/basic/package-info.java
new file mode 100644
index 00000000..5ecf339c
--- /dev/null
+++ b/basic/src/test/java/org/linguafranca/pwdb/basic/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2025. Jo Rabin
+ * Copyright (c) 2026. Sergio Lissner
+ *
+ * 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.
+ *
+ */
+@NullMarked
+package org.linguafranca.pwdb.basic;
+
+import org.jspecify.annotations.NullMarked;
\ No newline at end of file
diff --git a/basic/src/test/resources/junit-platform.properties b/basic/src/test/resources/junit-platform.properties
new file mode 100644
index 00000000..2115a2f1
--- /dev/null
+++ b/basic/src/test/resources/junit-platform.properties
@@ -0,0 +1 @@
+junit.jupiter.execution.parallel.enabled = true
diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml
deleted file mode 100644
index a8c1de44..00000000
--- a/bitbucket-pipelines.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-# 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 f4095dec..5fb14aab 100644
--- a/database/pom.xml
+++ b/database/pom.xml
@@ -1,31 +1,71 @@
-
-
- KeePassJava2-parent
- org.linguafranca.pwdb
- 2.2.3-SNAPSHOT
- ../pom.xml
-
- 4.0.0
+
- database
- PWDB :: Database
- Contains root definitions of PWDB Databases etc.
+
+
+ KeePassJava2.parent
+ mh.keepass
+ 1.0.0.1
+ ../pom.xml
+
+ 4.0.0
-
-
- org.jetbrains
- annotations
- 24.1.0
-
-
- com.google.guava
- guava
- 33.2.1-android
-
+ database
+ PWDB :: Database
+ Contains root definitions of PWDB Databases etc.
+
+
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+
- org.bouncycastle
- bcpkix-jdk18on
+
+ commons-io
+ commons-io
-
+
+ com.google.code.findbugs
+ jsr305
+ 3.0.2
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ test-jar
+
+
+
+
+
+
\ 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 7f03b64e..58dc7a5b 100644
--- a/database/src/main/java/org/linguafranca/pwdb/Credentials.java
+++ b/database/src/main/java/org/linguafranca/pwdb/Credentials.java
@@ -1,17 +1,18 @@
/*
- * Copyright 2015 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.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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;
diff --git a/database/src/main/java/org/linguafranca/pwdb/Database.java b/database/src/main/java/org/linguafranca/pwdb/Database.java
index 632f35ae..a92231a7 100644
--- a/database/src/main/java/org/linguafranca/pwdb/Database.java
+++ b/database/src/main/java/org/linguafranca/pwdb/Database.java
@@ -1,22 +1,23 @@
/*
- * Copyright 2015 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.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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.jetbrains.annotations.Nullable;
+import org.jspecify.annotations.Nullable;
import java.io.IOException;
import java.io.OutputStream;
@@ -49,26 +50,26 @@
* the caller without affecting the underlying database structure, however changes
* to the Groups and Entries contained in the lists do modify the database.
*/
-public interface Database , G extends Group, E extends Entry, I extends Icon> {
+public interface Database {
/**
* get the root group for the database
* @return the root group
*/
- G getRootGroup();
+ Group getRootGroup();
/**
* Create a new Group
* @return the group created
*/
- G newGroup();
+ Group newGroup();
/**
* Create a new named Group
* @param name the name of the group
* @return the group created
*/
- G newGroup(String name);
+ Group newGroup(String name);
/**
* Create a new Group copying the details of the supplied group, but not copying its children
@@ -77,19 +78,19 @@ public interface Database , G extends Group group);
+ Group newGroup(Group group);
/**
* Create a new Entry
* @return the entry created
*/
- E newEntry();
+ Entry newEntry();
/**
* Create a new Entry with a title
* @return the entry created
*/
- E newEntry(String title);
+ Entry newEntry(String title);
/**
* Create a new Entry copying the details of the supplied entry
@@ -98,27 +99,27 @@ public interface Database , G extends Group entry);
+ Entry newEntry(Entry entry);
/**
* Create a new default icon
* @return the created icon
*/
- I newIcon();
+ Icon newIcon();
/**
* Create a new icon with a specified index
* @param i the index of the icon to create
* @return the created icon
*/
- I newIcon(Integer i);
+ Icon newIcon(Integer i);
/**
* Find an entry with this UUID anywhere in the database except the recycle bin
* @param uuid the UUID
* @return an entry or null if not found
*/
- @Nullable E findEntry(UUID uuid);
+ @Nullable Entry findEntry(UUID uuid);
/**
* Delete an entry with this UUID from anywhere in the database except the recycle bin
@@ -133,7 +134,7 @@ public interface Database , G extends Group, G extends Group, G extends Group visitor);
+ void visit(Visitor 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(Group group, Visitor visitor);
/**
* Find all entries that match the criteria
@@ -192,7 +193,7 @@ public interface Database , G extends Group findEntries(Entry.Matcher matcher);
+ List findEntries(Entry.Matcher matcher);
/**
* Find all entries that match {@link Entry#match(String)}
@@ -200,7 +201,7 @@ public interface Database , G extends Group findEntries(String find);
+ List findEntries(String find);
/**
* Gets the name of the database or null if not supported
@@ -237,10 +238,20 @@ public interface Database , G extends Group void save(StreamFormat streamFormat, Credentials credentials, OutputStream outputStream) throws IOException;
+ /**
+ * Save the database to a stream and closes the stream - avoiding checked exception
+ */
+ void saveNx(StreamFormat streamFormat, Credentials credentials, OutputStream outputStream);
+
/**
* Get the format the database was loaded from
*/
@@ -264,12 +275,13 @@ public interface Database , G extends Group listShouldProtect();
/**
* Get the default means of storage of unprotected and protected property values
*/
- public PropertyValue.Strategy getPropertyValueStrategy();
+ PropertyValue.Strategy getPropertyValueStrategy();
/**
* Set the default means of storage of unprotected and protected property values
diff --git a/database/src/main/java/org/linguafranca/pwdb/Entry.java b/database/src/main/java/org/linguafranca/pwdb/Entry.java
index 2c2a95a3..1d7acf64 100644
--- a/database/src/main/java/org/linguafranca/pwdb/Entry.java
+++ b/database/src/main/java/org/linguafranca/pwdb/Entry.java
@@ -1,23 +1,23 @@
/*
- * Copyright 2015 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.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import org.jspecify.annotations.Nullable;
import java.util.*;
@@ -43,30 +43,40 @@
*
* @author Jo
*/
-public interface Entry , G extends Group, E extends Entry, I extends Icon> {
+public interface Entry {
/**
* Standard properties are attributes of Entries that are accessible either by
* dedicated methods, such as getPassword, or by {@link #getProperty(String)}
*/
+ String STANDARD_PROPERTY_NAME_TITLE = "Title";
String STANDARD_PROPERTY_NAME_USER_NAME = "UserName";
String STANDARD_PROPERTY_NAME_PASSWORD = "Password";
String STANDARD_PROPERTY_NAME_URL = "URL";
- String STANDARD_PROPERTY_NAME_TITLE = "Title";
String STANDARD_PROPERTY_NAME_NOTES = "Notes";
- List STANDARD_PROPERTY_NAMES = Collections.unmodifiableList(Arrays.asList(
+ /** allows for static import of name */
+ interface STANDARD_PROPERTY_NAME {
+ String TITLE = STANDARD_PROPERTY_NAME_TITLE;
+ String USER_NAME = STANDARD_PROPERTY_NAME_USER_NAME;
+ String PASSWORD = STANDARD_PROPERTY_NAME_PASSWORD;
+ String URL = STANDARD_PROPERTY_NAME_URL;
+ String NOTES = STANDARD_PROPERTY_NAME_NOTES;
+ }
+
+ List STANDARD_PROPERTY_NAMES = List.of(
+ STANDARD_PROPERTY_NAME_TITLE,
STANDARD_PROPERTY_NAME_USER_NAME,
STANDARD_PROPERTY_NAME_PASSWORD,
STANDARD_PROPERTY_NAME_URL,
- STANDARD_PROPERTY_NAME_TITLE,
- STANDARD_PROPERTY_NAME_NOTES));
+ STANDARD_PROPERTY_NAME_NOTES);
/**
* Interface to implement when using the {@link #match(Entry.Matcher)}
* method
*/
interface Matcher {
+ // needs to be raw type so that we can use lambdas
boolean matches(Entry entry);
}
@@ -104,7 +114,7 @@ interface Matcher {
* @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);
+ @Nullable String getProperty(String name);
/**
* Gets the value of a property as a PropertyValue.
@@ -118,7 +128,8 @@ interface Matcher {
PropertyValue getPropertyValue(String name);
/**
- * Sets the value of a property. Use of this method is not recommended for fields with protected values.
+ * Sets the value of a property as a String in memory.
+ * 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.
@@ -129,10 +140,10 @@ interface Matcher {
* non-standard properties are not supported
* @see Database#supportsNonStandardPropertyNames()
*/
- void setProperty(String name, String value);
+ Entry setProperty(String name, String value);
/**
- * Sets the value of a property as a property value. The method does not check whether the
+ * Sets the value of a property as a property value in memory. 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.
@@ -143,10 +154,71 @@ interface Matcher {
* non-standard properties are not supported
* @see Database#supportsNonStandardPropertyNames()
*/
- void setPropertyValue(String name, PropertyValue value);
+ Entry setPropertyValue(String name, PropertyValue value);
+
+ /**
+ * Adds (or modifies) the value of a property as a property value in memory. The method uses the
+ * PropertyValue.Strategy to determine how to determine the storage format in memory.
+ *
+ *
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()
+ */
+ Entry addProperty(String name, byte[] value);
/**
- * Removes this non-standard property, if it exists.
+ * Adds (or modifies) the value of a property as a property value in memory. The method uses the
+ * PropertyValue.Strategy to determine how to determine the storage format in memory.
+ *
+ *
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()
+ */
+ Entry addProperty(String name, char[] value);
+
+ /**
+ * Adds (or modifies) the value of a property as a property value in memory. The method uses the
+ * PropertyValue.Strategy to determine how to determine the storage format in memory.
+ *
+ *
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()
+ */
+ Entry addProperty(String name, CharSequence value);
+
+ /**
+ * Adds a newly created entry to the Group parent of the current Entry, if there is one. Intended primary for
+ * fluid building of entries within a group.
+ *
+ * @throws IllegalStateException if there is no parent
+ * @param name the title of the entry
+ * @return the newly created entry
+ */
+ Entry addEntry(String name);
+
+ /**
+ * Adds a newly created entry to the Group parent of the current Entry, if there is one. Intended primary for
+ * fluid building of entries within a group.
+ *
+ * @throws IllegalStateException if there is no parent
+ * @return the newly created entry
+ */
+ Entry addEntry();
+
+ /**
+ * Removes this property, if it exists and if it is a non-standard property
*
* @return true if the property exists and was removed, false otherwise
* @param name the value of the property to remove
@@ -174,7 +246,7 @@ interface Matcher {
* @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);
+ byte@Nullable [] getBinaryProperty(String name);
/**
* Sets the value of a binary property.
@@ -209,11 +281,17 @@ interface Matcher {
*/
List getBinaryPropertyNames();
+ /**
+ * Get the database this entry belongs to
+ * @return a database
+ */
+ Database getDatabase();
+
/**
* Get the parent of this entry
* @return a parent or null if this entry currently does not have a parent
*/
- @Nullable G getParent();
+ @Nullable Group getParent();
/**
* Get the UUID of this entry. Databases (like KDB) that do not natively support
@@ -221,7 +299,7 @@ interface Matcher {
*
* @return a UUID
*/
- @NotNull UUID getUuid();
+ UUID getUuid();
/**
* Get the username field of this entry
@@ -248,6 +326,7 @@ interface Matcher {
*
Implementations should Touch LastAccessedTime when this method is called.
*
* @return a password
+ * @deprecated use {@link #getPropertyValue(String)} with {@link #STANDARD_PROPERTY_NAME_PASSWORD}
*/
@Deprecated
String getPassword();
@@ -258,7 +337,7 @@ interface Matcher {
*
Implementations should Touch LastModifiedTime when this method is called.
*
* @param pass a password
- * @deprecated use {@link #setPropertyValue(String, PropertyValue)}
+ * @deprecated use {@link #setPropertyValue(String, PropertyValue)} with {@link #STANDARD_PROPERTY_NAME_PASSWORD}
*/
@Deprecated
void setPassword(String pass);
@@ -342,13 +421,13 @@ interface Matcher {
* Returns the {@link Icon} associated with this entry.
* @return an Icon
*/
- I getIcon();
+ Icon getIcon();
/**
* Sets the {@link Icon} associated with this entry.
* @param icon an Icon
*/
- void setIcon(I icon);
+ void setIcon(Icon icon);
/**
* Returns the date at which any value was retrieved from this entry.
diff --git a/database/src/main/java/org/linguafranca/pwdb/Group.java b/database/src/main/java/org/linguafranca/pwdb/Group.java
index 152a3f12..dae9cf23 100644
--- a/database/src/main/java/org/linguafranca/pwdb/Group.java
+++ b/database/src/main/java/org/linguafranca/pwdb/Group.java
@@ -1,23 +1,22 @@
/*
- * Copyright 2015 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.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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.jetbrains.annotations.NotNull;
-
import javax.annotation.Nullable;
import java.util.List;
import java.util.UUID;
@@ -37,7 +36,7 @@
*
* @author Jo
*/
-public interface Group , G extends Group, E extends Entry, I extends Icon> {
+public interface Group {
/**
* Returns true if this is the root group of a database
*/
@@ -54,7 +53,7 @@ public interface Group , G extends Group, G extends Group, G extends Group getGroups();
+ List getGroups();
/**
* Returns the number of groups that are direct children of this group
*
*
It's possible that returning a list as in {@link #getGroups()} may incur significantly
- * more overhead so use this method if only the count is reuqired
+ * more overhead so use this method if only the count is required
*/
int getGroupsCount();
@@ -90,7 +89,15 @@ public interface Group , G extends Group, G extends Group findGroups(String groupName);
+ List findGroups(String groupName);
/**
* Removes the group supplied from this group. The group removed
- * no longer has a parent and so in not part of the database any more unless
+ * no longer has a parent and so in no longer part of the database unless
* it is re-added to another group.
*
- * If the group is incompatible with the databse an exception is thrown.
+ * If the group is incompatible with the database an exception is thrown.
*
* If the group is not present no error is thrown.
* @param group the group to remove
* @return the group passed for removal
* @throws IllegalArgumentException if the group is not a child of this group
*/
- G removeGroup(G group);
+ Group removeGroup(Group group);
/**
* Returns a modifiable by the caller list of entries contained in this group.
*/
- List extends E> getEntries();
+ List getEntries();
/**
* Returns the number of entries in this group
*
*
It's possible that returning a list as in {@link #getEntries()} may incur significantly
- * more overhead so use this method if only the count is reuqired
+ * more overhead so use this method if only the count is required
*/
int getEntriesCount();
@@ -135,14 +142,14 @@ public interface 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
+ *
Note: finding within recycle bin is supported, recursion into recycle bin is inhibited
*
* @param match the text to match
- * @param recursive whether to include sub groups in the process
+ * @param recursive whether to include subgroups in the process
* @return a modifiable-by-caller list
* @see Entry#match(String)
*/
- List extends E> findEntries(String match, boolean recursive);
+ List findEntries(String match, boolean recursive);
/**
* Finds all entries in this group that match using the matcher supplied.
@@ -150,14 +157,14 @@ public interface Group , G extends GroupEntry match is described under {@link Entry#match(Entry.Matcher)}
*
- *
Note: finding within recycle bin is supported, recusion into recycle bin is inhibited
+ *
Note: finding within recycle bin is supported, recursion into recycle bin is inhibited
*
- * @param matcher the mathcher to use
- * @param recursive whether to include sub groups in the process
+ * @param matcher the matcher to use
+ * @param recursive whether to include subgroups in the process
* @return a modifiable-by-caller list
* @see Entry#match(Entry.Matcher)
*/
- List extends E> findEntries(Entry.Matcher matcher, boolean recursive);
+ List findEntries(Entry.Matcher matcher, boolean recursive);
/**
* Adds an entry to this group removing it from another group
@@ -165,7 +172,20 @@ public interface Group , G extends Group, G extends Group parent);
+ void copy(Group parent);
/**
* Returns an XPath-like string of the names of Groups from the Root
@@ -210,10 +230,10 @@ public interface Group , G extends Group getFactoryFor(String prop
newUnprotected();
}
+ /**
+ * The default implementation of the Strategy interface in which the only protected property is Password.
+ * It uses SealedStore and BytesStore for protected and unprotected properties. It is immutable.
+ */
class Default implements Strategy {
@Override
public List getProtectedProperties() {
- //noinspection ArraysAsListWithZeroOrOneArgument
- return new ArrayList<>(Arrays.asList(Entry.STANDARD_PROPERTY_NAME_PASSWORD));
+ return List.of(Entry.STANDARD_PROPERTY_NAME_PASSWORD);
}
@Override
@@ -122,15 +140,31 @@ public Factory extends PropertyValue> newUnprotected() {
return BytesStore.getFactory();
}
}
+
+ /**
+ * A mutable implementation of the Strategy interface, which allows the protected properties to be set.
+ * By default, the only protected property is Password.
+ * It uses SealedStore and BytesStore for protected and unprotected properties.
+ */
+ class MutableProtectionStrategy extends PropertyValue.Strategy.Default {
+ List protectedProperties = new ArrayList<>();
+ {
+ protectedProperties.add(Entry.STANDARD_PROPERTY_NAME_PASSWORD);
+ }
+ @Override
+ public List getProtectedProperties() {
+ return protectedProperties;
+ }
+ }
}
/**
- * Values are stored as strings.
+ * Property Values are stored as strings.
*/
class StringStore implements PropertyValue {
private final String value;
private final static PropertyValue.Factory factory =
- new PropertyValue.Factory(){
+ new PropertyValue.Factory<>(){
@Override
public StringStore of(CharSequence aCharSequence) {
@@ -195,7 +229,7 @@ class BytesStore implements PropertyValue, Serializable {
private final byte[] value;
private final static PropertyValue.Factory factory =
- new PropertyValue.Factory(){
+ new PropertyValue.Factory<>(){
@Override
public BytesStore of(CharSequence aCharSequence) {
@@ -216,6 +250,7 @@ public BytesStore of(byte[] value) {
public static PropertyValue.Factory getFactory() {
return factory;
}
+
public BytesStore(CharSequence aString) {
this.value = charSequenceToBytes(aString);
}
@@ -230,13 +265,13 @@ public BytesStore(byte [] value) {
@Override
public String getValueAsString() {
- return new String(this.value);
+ return new String(this.value, StandardCharsets.UTF_8);
}
@Override
public CharSequence getValue() {
ByteBuffer bb = ByteBuffer.wrap(this.value);
- return Charsets.UTF_8.decode(bb);
+ return StandardCharsets.UTF_8.decode(bb);
}
@Override
@@ -305,7 +340,7 @@ private byte[] getBytes() {
}
private final static PropertyValue.Factory factory =
- new PropertyValue.Factory(){
+ new PropertyValue.Factory<>(){
@Override
public SealedStore of(CharSequence aCharSequence) {
@@ -415,7 +450,7 @@ public SealedStore(CharSequence charSequence) {
@Override
public String getValueAsString() {
- return new String(getBytes());
+ return new String(getBytes(), StandardCharsets.UTF_8);
}
@Override
diff --git a/database/src/main/java/org/linguafranca/pwdb/SerializableDatabase.java b/database/src/main/java/org/linguafranca/pwdb/SerializableDatabase.java
index 74e7a961..a956839e 100644
--- a/database/src/main/java/org/linguafranca/pwdb/SerializableDatabase.java
+++ b/database/src/main/java/org/linguafranca/pwdb/SerializableDatabase.java
@@ -1,17 +1,18 @@
/*
- * Copyright 2015 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.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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;
diff --git a/database/src/main/java/org/linguafranca/pwdb/StreamConfiguration.java b/database/src/main/java/org/linguafranca/pwdb/StreamConfiguration.java
index 4b717495..f6f3709f 100644
--- a/database/src/main/java/org/linguafranca/pwdb/StreamConfiguration.java
+++ b/database/src/main/java/org/linguafranca/pwdb/StreamConfiguration.java
@@ -1,3 +1,20 @@
+/*
+ * 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.
+ * 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;
/**
diff --git a/database/src/main/java/org/linguafranca/pwdb/StreamFormat.java b/database/src/main/java/org/linguafranca/pwdb/StreamFormat.java
index bc8779bc..ed006e17 100644
--- a/database/src/main/java/org/linguafranca/pwdb/StreamFormat.java
+++ b/database/src/main/java/org/linguafranca/pwdb/StreamFormat.java
@@ -1,17 +1,18 @@
/*
- * Copyright 2015 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.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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;
diff --git a/database/src/main/java/org/linguafranca/pwdb/Visitor.java b/database/src/main/java/org/linguafranca/pwdb/Visitor.java
index a7e3536b..fe767f41 100644
--- a/database/src/main/java/org/linguafranca/pwdb/Visitor.java
+++ b/database/src/main/java/org/linguafranca/pwdb/Visitor.java
@@ -1,17 +1,18 @@
/*
- * Copyright 2015 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.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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;
@@ -23,24 +24,24 @@
*
* @author jo
*/
-public interface Visitor , G extends Group, E extends Entry, I extends Icon> {
+public interface Visitor {
/**
* Called on entry to a group visit
* @param group the group being visited
*/
- void startVisit(G group);
+ void startVisit(Group group);
/**
* Called on exit from a group visit
* @param group the group being exited
*/
- void endVisit(G group);
+ void endVisit(Group group );
/**
* Called on visit to an entry
* @param entry the entry being visited
*/
- void visit(E entry);
+ void visit(Entry entry);
/**
* called to determine whether to visit entries before subgroups, or not
@@ -51,17 +52,16 @@ public interface Visitor , G extends Group, G extends Group, E extends Entry, I extends Icon>
- implements Visitor {
+ abstract class Default implements Visitor {
@Override
- public void startVisit(G group) {}
+ public void startVisit(Group group) {}
@Override
- public void endVisit(G group) {}
+ public void endVisit(Group group) {}
@Override
- public void visit(E entry) {}
+ public void visit(Entry entry) {}
@Override
public boolean isEntriesFirst() {
@@ -72,8 +72,7 @@ public boolean isEntriesFirst() {
/**
* Visitor prints the Groups and Entries it visits to console
*/
- class Print , G extends Group, E extends Entry, I extends Icon>
- extends Default {
+ class Print extends Default{
private final PrintStream printStream;
@@ -86,12 +85,12 @@ public Print(PrintStream out) {
}
@Override
- public void startVisit(G group) {
+ public void startVisit(Group group ) {
printStream.println(group.toString());
}
@Override
- public void visit(E entry) {
+ public void visit(Entry 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/abstractdb/AbstractDatabase.java
similarity index 74%
rename from database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java
rename to database/src/main/java/org/linguafranca/pwdb/abstractdb/AbstractDatabase.java
index e8cada25..03ffb624 100644
--- a/database/src/main/java/org/linguafranca/pwdb/base/AbstractDatabase.java
+++ b/database/src/main/java/org/linguafranca/pwdb/abstractdb/AbstractDatabase.java
@@ -1,23 +1,25 @@
/*
- * Copyright 2015 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.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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.base;
+package org.linguafranca.pwdb.abstractdb;
import org.linguafranca.pwdb.*;
+import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@@ -27,7 +29,7 @@
*
* @author Jo
*/
-public abstract class AbstractDatabase, G extends Group, E extends Entry, I extends Icon> implements Database {
+public abstract class AbstractDatabase implements Database {
private boolean isDirty;
@@ -41,69 +43,69 @@ 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(Group group, Visitor visitor) {
if (visitor.isEntriesFirst()) {
- for (E entry : group.getEntries()) {
+ for (Entry entry : group.getEntries()) {
visitor.visit(entry);
}
}
- for (G g : group.getGroups()) {
+ for (Group g : group.getGroups()) {
visitor.startVisit(g);
visit(g, visitor);
visitor.endVisit(g);
}
if (!visitor.isEntriesFirst()) {
- for (E entry : group.getEntries()) {
+ for (Entry entry : group.getEntries()) {
visitor.visit(entry);
}
}
}
@Override
- public List extends E> findEntries(Entry.Matcher matcher) {
+ public List findEntries(Entry.Matcher matcher) {
return getRootGroup().findEntries(matcher, true);
}
@Override
- public List extends E> findEntries(String find) {
+ public List findEntries(String find) {
return getRootGroup().findEntries(find, true);
}
@Override
- public G newGroup(String name) {
- G result = newGroup();
+ public Group newGroup(String name) {
+ Group result = newGroup();
result.setName(name);
return result;
}
@Override
- public G newGroup(Group ,?,?,?> group) {
- G result = newGroup();
+ public Group newGroup(Group group) {
+ Group result = newGroup();
result.setName(group.getName());
result.setIcon(this.newIcon(group.getIcon().getIndex()));
return result;
}
@Override
- public E newEntry(String title) {
- E result = newEntry();
+ public Entry newEntry(String title) {
+ Entry result = newEntry();
result.setTitle(title);
return result;
}
@Override
- public E newEntry(Entry,?,?,?> entry) {
- E result = newEntry();
+ public Entry newEntry(Entry entry) {
+ Entry result = newEntry();
for (String propertyName: entry.getPropertyNames()) {
try {
// all implementations must support setting of STANDARD_PROPERTY_NAMES
@@ -130,8 +132,8 @@ public E newEntry(Entry,?,?,?> entry) {
}
@Override
- public E findEntry(final UUID uuid) {
- List extends E> entries = findEntries(entry -> entry.getUuid().equals(uuid));
+ public Entry findEntry(final UUID uuid) {
+ List extends Entry> entries = findEntries(entry -> entry.getUuid().equals(uuid));
if (entries.size() > 1) {
throw new IllegalStateException("Two entries same UUID");
}
@@ -143,7 +145,7 @@ public E findEntry(final UUID uuid) {
@Override
public boolean deleteEntry(final UUID uuid) {
- E e = findEntry(uuid);
+ Entry e = findEntry(uuid);
if (e == null) {
return false;
}
@@ -158,13 +160,13 @@ public boolean deleteEntry(final UUID uuid) {
}
@Override
- public G findGroup(final UUID uuid){
- final List groups = new ArrayList<>();
- visit(new Visitor.Default() {
+ public Group 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) {
+ public void startVisit(Group group) {
if (!recycle && group.getUuid().equals(uuid)) {
groups.add(group);
}
@@ -174,7 +176,7 @@ public void startVisit(G group) {
}
@Override
- public void endVisit(G group) {
+ public void endVisit(Group group) {
if (group.isRecycleBin()) {
recycle = false;
}
@@ -191,7 +193,7 @@ public void endVisit(G group) {
@Override
public boolean deleteGroup(final UUID uuid) {
- G g = findGroup(uuid);
+ Group g = findGroup(uuid);
if (g==null) {
return false;
}
@@ -207,14 +209,14 @@ public boolean deleteGroup(final UUID uuid) {
@Override
public void emptyRecycleBin() {
- G recycle = getRecycleBin();
+ Group recycle = getRecycleBin();
if (recycle == null) {
return;
}
- for (G g: recycle.getGroups()){
+ for (Group g: recycle.getGroups()){
recycle.removeGroup(g);
}
- for (E e: recycle.getEntries()){
+ for (Entry e: recycle.getEntries()){
recycle.removeEntry(e);
}
}
@@ -231,7 +233,7 @@ public boolean supportsBinaryProperties() {
@Override
public boolean supportsRecycleBin() {
- return true;
+ return false;
}
@Override
@@ -262,4 +264,21 @@ public boolean supportsPropertyValueStrategy(){
return false;
}
+ @Override
+ public void saveNx(Credentials credentials, OutputStream outputStream) {
+ try {
+ save(credentials, outputStream);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void saveNx(StreamFormat streamFormat, Credentials credentials, OutputStream outputStream) {
+ try {
+ save(streamFormat, credentials, outputStream);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
}
diff --git a/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java b/database/src/main/java/org/linguafranca/pwdb/abstractdb/AbstractEntry.java
similarity index 75%
rename from database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java
rename to database/src/main/java/org/linguafranca/pwdb/abstractdb/AbstractEntry.java
index 47f233d2..145617f1 100644
--- a/database/src/main/java/org/linguafranca/pwdb/base/AbstractEntry.java
+++ b/database/src/main/java/org/linguafranca/pwdb/abstractdb/AbstractEntry.java
@@ -1,20 +1,21 @@
/*
- * Copyright 2015 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.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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.base;
+package org.linguafranca.pwdb.abstractdb;
import org.linguafranca.pwdb.*;
@@ -23,7 +24,7 @@
*
* @author Jo
*/
-public abstract class AbstractEntry, G extends Group, E extends Entry, I extends Icon> implements Entry {
+public abstract class AbstractEntry implements Entry {
@Override
public boolean matchTitle(String text){
@@ -57,7 +58,7 @@ public boolean match(Entry.Matcher matcher) {
@Override
public String getPath() {
- G parent = this.getParent();
+ Group parent = this.getParent();
String result = "";
if (parent != null) {
result = parent.getPath();
@@ -130,9 +131,34 @@ public PropertyValue getPropertyValue(String name) {
}
@Override
- public void setPropertyValue(String name, PropertyValue value) {
+ public Entry setPropertyValue(String name, PropertyValue value) {
+ throw new UnsupportedOperationException();
+ }
+
+ public Entry addProperty(String name, byte[] value){
+ throw new UnsupportedOperationException();
+ }
+ public Entry addProperty(String name, char[] value){
+ throw new UnsupportedOperationException();
+ }
+ public Entry addProperty(String name, CharSequence value){
throw new UnsupportedOperationException();
}
+ public Entry addEntry(){
+ if (this.getParent()!=null) {
+ return this.getParent().addEntry();
+ }
+ throw new IllegalStateException("Entry has no group parent");
+ }
+
+ public Entry addEntry(String title){
+ if (this.getParent()!=null) {
+ return this.getParent().addEntry(title);
+ }
+ throw new IllegalStateException("Entry has no group parent");
+ }
+
+
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/abstractdb/AbstractGroup.java
similarity index 52%
rename from database/src/main/java/org/linguafranca/pwdb/base/AbstractGroup.java
rename to database/src/main/java/org/linguafranca/pwdb/abstractdb/AbstractGroup.java
index cf13c0af..6a4c5f48 100644
--- a/database/src/main/java/org/linguafranca/pwdb/base/AbstractGroup.java
+++ b/database/src/main/java/org/linguafranca/pwdb/abstractdb/AbstractGroup.java
@@ -1,25 +1,24 @@
/*
- * Copyright 2015 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.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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.base;
+package org.linguafranca.pwdb.abstractdb;
-import org.linguafranca.pwdb.Database;
import org.linguafranca.pwdb.Entry;
import org.linguafranca.pwdb.Group;
-import org.linguafranca.pwdb.Icon;
import java.util.ArrayList;
import java.util.List;
@@ -30,52 +29,52 @@
*
* @author Jo
*/
-public abstract class AbstractGroup, G extends org.linguafranca.pwdb.base.AbstractGroup, E extends Entry, I extends Icon> implements Group {
+public abstract class AbstractGroup implements Group {
@Override
- public List extends G> findGroups(String group1) {
- ArrayList result = new ArrayList<>();
- for (G g: getGroups()) {
- if (g.getName().equals(group1)) {
- result.add(g);
- }
- }
- return result;
+ public Entry addEntry() {
+ Entry entry = getDatabase().newEntry();
+ return addEntry(entry);
}
@Override
- public List extends E> findEntries(String find, boolean recursive) {
- /*
- * Local helper class to avoid violating DRY in {@link AbstractGroup#findEntries(String, boolean)}.
- * Would-be lambda in Java 8.
- */
- class TextMatcher implements Entry.Matcher {
-
- private final String text;
+ public Entry addEntry(String title) {
+ Entry entry = getDatabase().newEntry(title);
+ return addEntry(entry);
+ }
- private TextMatcher(String text) {
- this.text = text;
- }
+ @Override
+ public Group addGroup(String name) {
+ Group group = getDatabase().newGroup(name);
+ return addGroup(group);
+ }
- @Override
- public boolean matches(Entry entry) {
- return entry.match(text);
+ @Override
+ public List findGroups(String group) {
+ ArrayList result = new ArrayList<>();
+ for (Group g: getGroups()) {
+ if (g.getName().equals(group)) {
+ result.add(g);
}
}
+ return result;
+ }
- return findEntries(new TextMatcher(find), recursive);
+ @Override
+ public List findEntries(String find, boolean recursive) {
+ return findEntries(entry -> entry.match(find), recursive);
}
@Override
- public List extends E> findEntries(Entry.Matcher matcher, boolean recursive) {
- List result = new ArrayList<>(getEntries().size());
- for (E entry: getEntries()){
+ public List findEntries(Entry.Matcher matcher, boolean recursive) {
+ List result = new ArrayList<>(getEntries().size());
+ for (Entry entry: getEntries()){
if (entry.match(matcher)){
result.add(entry);
}
}
if (recursive) {
- for (G group : getGroups()) {
+ for (Group group: getGroups()) {
// don't recurse into recycle bin
if (group.isRecycleBin()) {
continue;
@@ -87,9 +86,9 @@ public List extends E> findEntries(Entry.Matcher matcher, boolean recursive) {
}
@Override
- public void copy(Group extends Database, ? extends Group, ? extends Entry, ? extends Icon> parent) {
- for (Group,?,?,?> child : parent.getGroups()) {
- G addedGroup = addGroup(this.getDatabase().newGroup(child));
+ public void copy(Group parent) {
+ for (Group child : parent.getGroups()) {
+ Group addedGroup = addGroup(this.getDatabase().newGroup(child));
addedGroup.copy(child);
}
for (Entry entry : parent.getEntries()) {
@@ -106,7 +105,7 @@ public String getPath() {
parents.push(parent);
}
StringBuilder result = new StringBuilder("/");
- while (parents.size() > 0) {
+ while (!parents.isEmpty()) {
result.append(parents.pop().getName()).append("/");
}
return result.toString();
diff --git a/database/src/main/java/org/linguafranca/pwdb/abstractdb/package-info.java b/database/src/main/java/org/linguafranca/pwdb/abstractdb/package-info.java
new file mode 100644
index 00000000..6aafe5e8
--- /dev/null
+++ b/database/src/main/java/org/linguafranca/pwdb/abstractdb/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ * 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.
+ *
+ */
+@NullMarked
+package org.linguafranca.pwdb.abstractdb;
+
+import org.jspecify.annotations.NullMarked;
\ No newline at end of file
diff --git a/jackson/src/test/java/org/linguafranca/pwdb/JacksonRecycleBinTest.java b/database/src/main/java/org/linguafranca/pwdb/package-info.java
similarity index 54%
rename from jackson/src/test/java/org/linguafranca/pwdb/JacksonRecycleBinTest.java
rename to database/src/main/java/org/linguafranca/pwdb/package-info.java
index fc63749d..bf6b8ed1 100644
--- a/jackson/src/test/java/org/linguafranca/pwdb/JacksonRecycleBinTest.java
+++ b/database/src/main/java/org/linguafranca/pwdb/package-info.java
@@ -1,31 +1,28 @@
/*
- * Copyright 2023 Giuseppe Valente
+ * 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.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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 {
+/**
+ * This package contains the interfaces that define the org.linguafranca Database API.
+ *
+ * The API is designed to be a simple, low-level, read-write API for password databases.
+ *
+ * Class instances of the interfaces are contained in the sub-packages of this package.
+ */
+@NullMarked
+package org.linguafranca.pwdb;
- public JacksonRecycleBinTest() {
- try {
- database = new JacksonDatabase();
- } catch(Exception e) {
- e.printStackTrace();
- }
-
- }
-}
+import org.jspecify.annotations.NullMarked;
\ No newline at end of file
diff --git a/database/src/main/java/org/linguafranca/pwdb/protect/ProtectedDatabase.java b/database/src/main/java/org/linguafranca/pwdb/protect/ProtectedDatabase.java
index 7c50e16c..200f6209 100644
--- a/database/src/main/java/org/linguafranca/pwdb/protect/ProtectedDatabase.java
+++ b/database/src/main/java/org/linguafranca/pwdb/protect/ProtectedDatabase.java
@@ -1,7 +1,25 @@
+/*
+ * 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.
+ * 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.protect;
-import org.linguafranca.pwdb.*;
-import org.linguafranca.pwdb.base.AbstractDatabase;
+import org.linguafranca.pwdb.Entry;
+import org.linguafranca.pwdb.PropertyValue;
+import org.linguafranca.pwdb.abstractdb.AbstractDatabase;
import java.util.Collections;
import java.util.List;
@@ -10,28 +28,30 @@
* 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
+ * establishes {@link org.linguafranca.pwdb.PropertyValue.BytesStore} 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 {
+public abstract class ProtectedDatabase extends AbstractDatabase {
private PropertyValue.Strategy valueStrategy = new PropertyValue.Strategy.Default();
@Override
- public boolean shouldProtect(String propertyName){
+ public boolean shouldProtect(String propertyName) {
return valueStrategy.getProtectedProperties().contains(propertyName);
}
@Override
- public void setShouldProtect(String propertyName, boolean protect){
+ public void setShouldProtect(String propertyName, boolean protect) {
if (protect) {
- valueStrategy.getProtectedProperties().add(propertyName);
+ if (!valueStrategy.getProtectedProperties().contains(propertyName)) {
+ valueStrategy.getProtectedProperties().add(propertyName);
+ }
} else {
valueStrategy.getProtectedProperties().remove(propertyName);
}
}
@Override
- public List listShouldProtect(){
+ public List listShouldProtect() {
return Collections.unmodifiableList(valueStrategy.getProtectedProperties());
}
@@ -39,20 +59,22 @@ public List listShouldProtect(){
* Get the default means of storage of unprotected and protected property values
*/
@Override
- public PropertyValue.Strategy getPropertyValueStrategy(){
+ 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){
+ public void setPropertyValueStrategy(PropertyValue.Strategy strategy) {
this.valueStrategy = strategy;
}
@Override
- public boolean supportsPropertyValueStrategy(){
+ public boolean supportsPropertyValueStrategy() {
return true;
}
}
diff --git a/database/src/main/java/org/linguafranca/pwdb/protect/package-info.java b/database/src/main/java/org/linguafranca/pwdb/protect/package-info.java
new file mode 100644
index 00000000..acad64db
--- /dev/null
+++ b/database/src/main/java/org/linguafranca/pwdb/protect/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ * 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.
+ *
+ */
+@NullMarked
+package org.linguafranca.pwdb.protect;
+
+import org.jspecify.annotations.NullMarked;
\ No newline at end of file
diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java
index 776cf39e..529b07c2 100644
--- a/database/src/main/java/org/linguafranca/pwdb/security/Aes.java
+++ b/database/src/main/java/org/linguafranca/pwdb/security/Aes.java
@@ -1,3 +1,20 @@
+/*
+ * 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.
+ * 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.engines.AESEngine;
@@ -22,7 +39,12 @@
* AES may be used for Key Derivation and also as the underlying stream cipher
*
* The class is a singleton
+ *
+ * {@implNote This class is a singleton}
+ * {@implNote Warnings for the use of AES CBC etc. noted, however their continuing use is required for
+ * compatibility / interoperability with existing databases}
*/
+@SuppressWarnings({"deprecation"})
public class Aes implements CipherAlgorithm, KeyDerivationFunction {
/** UUID specifying that AES is to be used as the Key Derivation Function in KDBX */
diff --git a/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java b/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java
index a0ba2984..09d59494 100644
--- a/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java
+++ b/database/src/main/java/org/linguafranca/pwdb/security/Argon2.java
@@ -1,3 +1,20 @@
+/*
+ * 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.
+ * 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.generators.Argon2BytesGenerator;
diff --git a/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java b/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java
index 65ad1f5b..fd281032 100644
--- a/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java
+++ b/database/src/main/java/org/linguafranca/pwdb/security/ChaCha.java
@@ -1,3 +1,20 @@
+/*
+ * 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.
+ * 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;
diff --git a/database/src/main/java/org/linguafranca/pwdb/security/CipherAlgorithm.java b/database/src/main/java/org/linguafranca/pwdb/security/CipherAlgorithm.java
index bb1a1c0a..e1bc5524 100644
--- a/database/src/main/java/org/linguafranca/pwdb/security/CipherAlgorithm.java
+++ b/database/src/main/java/org/linguafranca/pwdb/security/CipherAlgorithm.java
@@ -1,3 +1,20 @@
+/*
+ * 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.
+ * 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 java.io.InputStream;
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 36d4a46e..32f9fdf8 100644
--- a/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java
+++ b/database/src/main/java/org/linguafranca/pwdb/security/Encryption.java
@@ -1,17 +1,18 @@
/*
- * Copyright 2015 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.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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;
diff --git a/database/src/main/java/org/linguafranca/pwdb/security/KeyDerivationFunction.java b/database/src/main/java/org/linguafranca/pwdb/security/KeyDerivationFunction.java
index 0e15663f..529dcfe8 100644
--- a/database/src/main/java/org/linguafranca/pwdb/security/KeyDerivationFunction.java
+++ b/database/src/main/java/org/linguafranca/pwdb/security/KeyDerivationFunction.java
@@ -1,3 +1,20 @@
+/*
+ * 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.
+ * 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 java.util.UUID;
diff --git a/database/src/main/java/org/linguafranca/pwdb/security/StreamEncryptor.java b/database/src/main/java/org/linguafranca/pwdb/security/StreamEncryptor.java
index b3fc4a15..e8827590 100644
--- a/database/src/main/java/org/linguafranca/pwdb/security/StreamEncryptor.java
+++ b/database/src/main/java/org/linguafranca/pwdb/security/StreamEncryptor.java
@@ -1,17 +1,18 @@
/*
- * Copyright 2015 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.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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;
diff --git a/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java b/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java
index 0a95db47..1738fb3b 100644
--- a/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java
+++ b/database/src/main/java/org/linguafranca/pwdb/security/VariantDictionary.java
@@ -1,7 +1,23 @@
+/*
+ * 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.
+ * 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.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import org.jspecify.annotations.Nullable;
import javax.annotation.concurrent.Immutable;
import java.nio.ByteBuffer;
@@ -10,7 +26,7 @@
import java.util.Map;
import java.util.UUID;
-import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.Objects.requireNonNull;
import static org.linguafranca.pwdb.security.VariantDictionary.EntryType.*;
/**
@@ -62,16 +78,16 @@ public static EntryType get(byte type) {
@Immutable
public static class Entry {
private final byte type;
- private final byte @NotNull [] value;
+ private final byte [] value;
private final ByteOrder byteOrder;
- public Entry(EntryType entryType, byte @NotNull [] value) {
+ public Entry(EntryType entryType, byte[] value) {
this(entryType, value, ByteOrder.LITTLE_ENDIAN);
}
- public Entry(EntryType entryType, byte @NotNull [] value, ByteOrder byteOrder) {
+ public Entry(EntryType entryType, byte[] value, ByteOrder byteOrder) {
this.type = entryType.value;
- this.value = checkNotNull(value, vnn);
+ this.value = requireNonNull(value, vnn);
this.byteOrder = byteOrder;
}
@@ -101,7 +117,7 @@ public int asInteger() {
return ByteBuffer.wrap(value).order(byteOrder).getInt();
}
- public byte @NotNull [] asByteArray() {
+ public byte[] asByteArray() {
return value;
}
}
@@ -147,7 +163,7 @@ public short getVersion() {
* @param key the key
* @return an entry, or null if no such entry exists
*/
- public @Nullable Entry get(@NotNull String key) {
+ public @Nullable Entry get(String key) {
return entries.get(key);
}
@@ -157,7 +173,7 @@ public short getVersion() {
* @param key the key to get
* @return the entry corresponding to the key
*/
- public @NotNull Entry mustGet(@NotNull String key) {
+ public Entry mustGet(String key) {
Entry entry = entries.get(key);
if (entry == null) {
throw new IllegalArgumentException("There is no entry with key " + key);
@@ -172,57 +188,57 @@ public short getVersion() {
* @param type the data type of the entry
* @param value a buffer containing an appropriate entry
*/
- public void put(@NotNull String key, EntryType type, byte @NotNull [] value) {
- entries.put(checkNotNull(key), new Entry(type, checkNotNull(value)));
+ public void put(String key, EntryType type, byte[] value) {
+ entries.put(requireNonNull(key), new Entry(type, requireNonNull(value)));
}
/**
* Put a UUID under the key defined
*/
- public void putUuid(@NotNull String key, UUID uuid) {
+ public void putUuid(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));
+ entries.put(requireNonNull(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));
+ public void putByteArray(String key, byte[] value) {
+ entries.put(requireNonNull(key, knn), new Entry(ARRAY, value));
}
/**
* Put a long as a signed64 under the key defined
*/
- public void putLong(@NotNull String key, long value) {
+ public void putLong(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));
+ entries.put(requireNonNull(key, knn), new Entry(INT64, buf));
}
/**
* Put a long as an unsigned64 under the key defined
*/
- public void putULong(@NotNull String key, long value) {
+ public void putULong(String key, long value) {
byte[] buf = new byte[8];
ByteBuffer bb = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN);
bb.putLong(value);
- entries.put(checkNotNull(key, knn), new Entry(UINT64, buf));
+ entries.put(requireNonNull(key, knn), new Entry(UINT64, buf));
}
- public void putInt(@NotNull String key, int value) {
+ public void putInt(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));
+ entries.put(requireNonNull(key, knn), new Entry(INT32, buf));
}
- public void putUInt(@NotNull String key, int value) {
+ public void putUInt(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));
+ entries.put(requireNonNull(key, knn), new Entry(UINT32, buf));
}
}
diff --git a/database/src/main/java/org/linguafranca/pwdb/security/package-info.java b/database/src/main/java/org/linguafranca/pwdb/security/package-info.java
new file mode 100644
index 00000000..61951d71
--- /dev/null
+++ b/database/src/main/java/org/linguafranca/pwdb/security/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ * 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.
+ *
+ */
+@NullMarked
+package org.linguafranca.pwdb.security;
+
+import org.jspecify.annotations.NullMarked;
\ No newline at end of file
diff --git a/database/src/main/java/org/linguafranca/util/MavenRunListener.java b/database/src/main/java/org/linguafranca/util/MavenRunListener.java
deleted file mode 100644
index 1839a645..00000000
--- a/database/src/main/java/org/linguafranca/util/MavenRunListener.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package org.linguafranca.util;
-
-import org.junit.runner.Description;
-import org.junit.runner.notification.Failure;
-import org.junit.runner.notification.RunListener;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.slf4j.Marker;
-import org.slf4j.MarkerFactory;
-
-/**
- * Helper class to identify the test being run by maven surefire in the log output. This
- * is particularly useful if there is a lot of "chatty" testing going on.
- *
- *
- *
- */
- @XmlAccessorType(XmlAccessType.FIELD)
- @XmlType(name = "", propOrder = {
- "windowAndKeystrokeSequence"
- })
- public static class Association {
-
- @XmlElementRefs({
- @XmlElementRef(name = "KeystrokeSequence", type = JAXBElement.class, required = false),
- @XmlElementRef(name = "Window", type = JAXBElement.class, required = false)
- })
- protected List> windowAndKeystrokeSequence;
-
- /**
- * Gets the value of the windowAndKeystrokeSequence property.
- *
- *
- * This accessor method returns a reference to the live list,
- * not a snapshot. Therefore any modification you make to the
- * returned list will be present inside the JAXB object.
- * This is why there is not a set method for the windowAndKeystrokeSequence property.
- *
- *
- * For example, to add a new item, do as follows:
- *
- * Objects of the following type(s) are allowed in the list
- * {@link JAXBElement }{@code <}{@link String }{@code >}
- * {@link JAXBElement }{@code <}{@link String }{@code >}
- *
- *
- */
- public List> getWindowAndKeystrokeSequence() {
- if (windowAndKeystrokeSequence == null) {
- windowAndKeystrokeSequence = new ArrayList>();
- }
- return this.windowAndKeystrokeSequence;
- }
-
- }
-
-}
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
deleted file mode 100644
index 4090b908..00000000
--- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/Binaries.java
+++ /dev/null
@@ -1,201 +0,0 @@
-//
-// 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 java.util.ArrayList;
-import java.util.List;
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-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;
-
-
-/**
- * Binary field for elements are centrally stored in this element.
- * The same Binary element may be referenced from many Entries.
- *
- *
- *
Java class for binaries complex type.
- *
- *
The following schema fragment specifies the expected content contained within this class.
- *
- *
- *
- *
- */
-@XmlAccessorType(XmlAccessType.FIELD)
-@XmlType(name = "binaries", propOrder = {
- "binary"
-})
-public class Binaries {
-
- @XmlElement(name = "Binary")
- protected List binary;
-
- /**
- * Gets the value of the binary property.
- *
- *
- * This accessor method returns a reference to the live list,
- * not a snapshot. Therefore any modification you make to the
- * returned list will be present inside the JAXB object.
- * This is why there is not a set method for the binary property.
- *
- *
- * For example, to add a new item, do as follows:
- *
- * getBinary().add(newItem);
- *
- *
- *
- *
- * Objects of the following type(s) are allowed in the list
- * {@link Binaries.Binary }
- *
- *
- */
- public List getBinary() {
- if (binary == null) {
- binary = new ArrayList();
- }
- return this.binary;
- }
-
-
- /**
- *
Java class for anonymous complex type.
- *
- *
The following schema fragment specifies the expected content contained within this class.
- *
- *
- *
- *
- */
- @XmlAccessorType(XmlAccessType.FIELD)
- @XmlType(name = "", propOrder = {
- "value"
- })
- public static class Binary {
-
- @XmlValue
- protected byte[] value;
- @XmlAttribute(name = "ID")
- protected Integer id;
- @XmlAttribute(name = "Compressed")
- @XmlJavaTypeAdapter(Adapter2 .class)
- protected Boolean compressed;
-
- /**
- * Gets the value of the value property.
- *
- * @return
- * possible object is
- * byte[]
- */
- public byte[] getValue() {
- return value;
- }
-
- /**
- * Sets the value of the value property.
- *
- * @param value
- * allowed object is
- * byte[]
- */
- public void setValue(byte[] value) {
- this.value = value;
- }
-
- /**
- * Gets the value of the id property.
- *
- * @return
- * possible object is
- * {@link Integer }
- *
- */
- public Integer getID() {
- return id;
- }
-
- /**
- * Sets the value of the id property.
- *
- * @param value
- * allowed object is
- * {@link Integer }
- *
- */
- public void setID(Integer value) {
- this.id = value;
- }
-
- /**
- * Gets the value of the compressed property.
- *
- * @return
- * possible object is
- * {@link String }
- *
- */
- public Boolean getCompressed() {
- if (compressed == null) {
- return new Adapter2().unmarshal("False");
- } else {
- return compressed;
- }
- }
-
- /**
- * Sets the value of the compressed property.
- *
- * @param value
- * allowed object is
- * {@link String }
- *
- */
- public void setCompressed(Boolean value) {
- this.compressed = value;
- }
-
- }
-
-}
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
deleted file mode 100644
index 50581d10..00000000
--- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/BinaryField.java
+++ /dev/null
@@ -1,164 +0,0 @@
-//
-// 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.XmlAttribute;
-import javax.xml.bind.annotation.XmlElement;
-import javax.xml.bind.annotation.XmlType;
-
-
-/**
- * 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.
- *
- *
- *
Java class for binaryField complex type.
- *
- *
The following schema fragment specifies the expected content contained within this class.
- *
- *
- *
- *
- */
- @XmlAccessorType(XmlAccessType.FIELD)
- @XmlType(name = "")
- public static class Value {
-
- @XmlAttribute(name = "Ref")
- protected Integer ref;
-
- /**
- * Gets the value of the ref property.
- *
- * @return
- * possible object is
- * {@link Integer }
- *
- */
- public Integer getRef() {
- return ref;
- }
-
- /**
- * Sets the value of the ref property.
- *
- * @param value
- * allowed object is
- * {@link Integer }
- *
- */
- public void setRef(Integer value) {
- this.ref = value;
- }
-
- }
-
-}
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
deleted file mode 100644
index cdd94c65..00000000
--- a/jaxb/src/generated/java/org/linguafranca/pwdb/kdbx/jaxb/binding/CustomData.java
+++ /dev/null
@@ -1,80 +0,0 @@
-//
-// 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 java.util.ArrayList;
-import java.util.List;
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-import javax.xml.bind.annotation.XmlAnyElement;
-import javax.xml.bind.annotation.XmlType;
-
-
-/**
- * Third party programs and plugins can put custom data here.
- * Unique element names should be used, e.g. "PluginName_ItemName".
- *
- *
- *
Java class for customData complex type.
- *
- *
The following schema fragment specifies the expected content contained within this class.
- *
- *