From b842121469aaa90270656dcbb29e1769af7d1c81 Mon Sep 17 00:00:00 2001 From: Francis Date: Sun, 8 Jan 2023 23:36:30 -0500 Subject: [PATCH 01/19] Bumping testcontainers version to 1.17.6, upgrading jackson version accordingly --- datasafe-business/pom.xml | 1 - .../impl/e2e/KeyStoreTypeCompareTest.java | 2 +- .../datasafe-storage-impl-s3/pom.xml | 32 +++++++++++++++++-- pom.xml | 4 +-- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/datasafe-business/pom.xml b/datasafe-business/pom.xml index 75e561e32..0a662fb81 100644 --- a/datasafe-business/pom.xml +++ b/datasafe-business/pom.xml @@ -168,7 +168,6 @@ org.testcontainers testcontainers - 1.11.2 org.awaitility diff --git a/datasafe-business/src/test/java/de/adorsys/datasafe/business/impl/e2e/KeyStoreTypeCompareTest.java b/datasafe-business/src/test/java/de/adorsys/datasafe/business/impl/e2e/KeyStoreTypeCompareTest.java index 6a72fb2d4..76e75fcc4 100644 --- a/datasafe-business/src/test/java/de/adorsys/datasafe/business/impl/e2e/KeyStoreTypeCompareTest.java +++ b/datasafe-business/src/test/java/de/adorsys/datasafe/business/impl/e2e/KeyStoreTypeCompareTest.java @@ -16,7 +16,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.testcontainers.shaded.org.apache.commons.lang.time.StopWatch; +import org.testcontainers.shaded.org.apache.commons.lang3.time.StopWatch; import java.io.InputStream; import java.io.OutputStream; diff --git a/datasafe-storage/datasafe-storage-impl-s3/pom.xml b/datasafe-storage/datasafe-storage-impl-s3/pom.xml index c4b73ac1e..5eb4c71a1 100644 --- a/datasafe-storage/datasafe-storage-impl-s3/pom.xml +++ b/datasafe-storage/datasafe-storage-impl-s3/pom.xml @@ -24,7 +24,7 @@ de.adorsys datasafe-types-api ${project.version} - + com.amazonaws aws-java-sdk-s3 @@ -32,8 +32,36 @@ com.amazonaws aws-java-sdk-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.dataformat + jackson-dataformat-cbor + + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + test + + + com.fasterxml.jackson.dataformat + jackson-dataformat-cbor + ${jackson.version} + test + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + test - org.slf4j diff --git a/pom.xml b/pom.xml index 42cccbd63..fc32051e4 100644 --- a/pom.xml +++ b/pom.xml @@ -97,7 +97,7 @@ false 1.8 1.8 - 1.11.2 + 1.17.6 0.8.4 2.5 1.7.25 @@ -372,7 +372,7 @@ analyze-only - true + false true javax.inject:javax.inject From befd01a7dd91d7a7d7801a140b05f74a554a00f7 Mon Sep 17 00:00:00 2001 From: Francis Pouatcha Date: Sat, 29 Apr 2023 07:54:14 -0400 Subject: [PATCH 02/19] Removing license change notice. Has been there for a year. --- README.md | 66 ++----------------------------------------------------- 1 file changed, 2 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 374713738..dac83b6d0 100644 --- a/README.md +++ b/README.md @@ -2,25 +2,6 @@ [![codecov](https://codecov.io/gh/adorsys/datasafe/branch/develop/graph/badge.svg)](https://codecov.io/gh/adorsys/datasafe) [![Maintainability](https://api.codeclimate.com/v1/badges/06ae7d4cafc3012cee85/maintainability)](https://codeclimate.com/github/adorsys/datasafe/maintainability) - -# Licensing model change to dual license: _AGPL v.3_ or _commercial license_ - -**Attention: this open-source project will change its licensing model as of _01.01.2022_!** - -Constantly evolving and extending scope, production traffic and support in open banking world call for high maintenance -and service investments on our part. - -Henceforth, Adorsys will offer all versions higher than v1.0.0.1 of Datasafe under a dual-license model. -Thus, this repository will be available either under Affero GNU General Public License v.3 (AGPL v.3) or -alternatively under a commercial license agreement. - -We would like to thank all our users for their trust so far and are convinced that we will be able to provide an even -better service going forward. - -For more information, advice for your implementation project or if your use case requires more time to adapt this -change, please contact us at [sales@adorsys.com](mailto:sales@adorsys.com). -For additional details please see the section [FAQ on Licensing Change](#faq-on-licensing-change). - # General information Datasafe is a cross-platform library that allows sharing and storing data and documents securely. It encrypts your data using **AES-GCM** algorithm and uses **CMS-envelopes** as encrypted content wrapper. CMS-envelope @@ -795,7 +776,7 @@ You can read JavaDoc [here](https://adorsys.github.io/datasafe/javadoc/latest/in * [Deployment to maven central](docs/general/deployment_maven_central.md) -# FAQ on Licensing Change +# FAQ on Licensing ## What is a dual-licensing model? Under a dual-licensing model, our product is available under two licenses @@ -806,48 +787,5 @@ If you are a developer or business that would like to review our products in det open-source projects and share the changes back to the community, the product repository is freely available under AGPL v3. If you are a business that would like to implement our products in a commercial setting and would like to protect your -individual changes, we offer the option to license our products under a commercial license. - -This change will still allow free access and ensure openness under AGPL v3 but with assurance of committing any -alterations or extensions back to the project and preventing redistribution of such implementations under commercial license. - -## Will there be any differences between the open-source and commercially licensed versions of your products? -Our public release frequency will be reduced as our focus shifts towards the continuous maintenance of the commercial version. -Nevertheless, we are committed to also provide open-source releases of our products on a regular basis as per our release policy. - -For customers with a commercial license, we will offer new intermediate releases in a more frequent pace. - -## Does this mean that this product is no longer open source? -No, the product will still be published and available on GitHub under an OSI-approved open-source license (AGPL v3). - -## What about adorsys’ commitment to open source? Will adorsys provide future product releases on GitHub? -We at adorsys are committed to continue actively participating in the open-source community. Our products remain -licensed under OSI-approved open-source licenses, and we are looking forward to expanding our product portfolio on GitHub even further. - -## How does the change impact me if I already use the open-source edition of your product? -All currently published versions until v1.0.0.1 will remain under their current Apache 2.0 license and its respective -requirements and you may continue using it as-is. To upgrade to future versions, you will be required to either abide -by the requirements of AGPL v3, including documenting and sharing your implemented changes to the product when distributing, -or alternatively approach us to obtain a commercial license. - -## What if I cannot adjust to the new licensing model until 01.01.2022? Can I extend the deadline? -We understand that adjustment to licensing changes can take time and therefore are open to discuss extension options -on an individual basis. For inquiries please contact us as [psd2@adorsys.com](mailto:sales@adorsys.com). - -## Which versions of the product are affected? -All versions of Datasafe after v1.0.0.1 will be affected by the licensing changes and move to a dual-licensing model. - -## What will happen to older, Apache 2.0 licensed product versions? -All older Apache 2.0 licensed versions prior and including v1.0.0.1 will remain available under their existing license. - -## What open-source products from Adorsys are affected by the licensing change? -The following products are affected: -- [XS2A Core](https://github.com/adorsys/xs2a) -- [XS2A Sandbox & ModelBank](https://github.com/adorsys/XS2A-Sandbox) -- [Open Banking Gateway](https://github.com/adorsys/open-banking-gateway) incl. [XS2A Adapters](https://github.com/adorsys/xs2a-adapter) -- [SmartAnalytics](https://github.com/adorsys/smartanalytics) -- [Datasafe](https://github.com/adorsys/datasafe) +individual changes, we offer the option to license our products under a commercial license. Please contact your service provider or approach us at [sales@adorsys.com](mailto:sales@adorsys.com) -## I’m using one of these products indirectly via some software integrator. How does the licensing change affect me? -The licensing change does not affect you as user, but it is relevant to your provider who has used our product in their -solution implementation. In case of uncertainty please contact your service provider or approach us at [sales@adorsys.com](mailto:sales@adorsys.com). From cccd3886877519d39f13da6420af8b30f37c6770 Mon Sep 17 00:00:00 2001 From: Francis Pouatcha Date: Sat, 29 Apr 2023 17:17:59 -0400 Subject: [PATCH 03/19] Reformulating the main page --- README.md | 86 +++++++++++++++++++++++++++---------------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index dac83b6d0..e75af9228 100644 --- a/README.md +++ b/README.md @@ -3,43 +3,43 @@ [![Maintainability](https://api.codeclimate.com/v1/badges/06ae7d4cafc3012cee85/maintainability)](https://codeclimate.com/github/adorsys/datasafe/maintainability) # General information -Datasafe is a cross-platform library that allows sharing and storing data and documents securely. It encrypts -your data using **AES-GCM** algorithm and uses **CMS-envelopes** as encrypted content wrapper. CMS-envelope -wraps and encrypts document encryption key using key encryption key that provides additional level of security. -For user private files, Datasafe uses CMS-envelope with symmetric encryption of data encryption key. For files -that are shared with other users (sent to their INBOX folder), Datasafe uses asymmetric encryption for -data encryption key, so only recipient (or multiple recipients) can read it. - -Datasafe is built with the idea to be as configurable as possible - it uses Dagger2 for dependency injection and modular -architecture to combine everything into the business layer, so the user can override any aspect he wants - i.e. to change -encryption algorithm or to turn path encryption off. Each module is as independent as it is possible - to be used separately. - -- Each user has private space that can reside on Amazon S3, minio, filesystem or anything else with proper adapter. -In his private space, each document and its path is encrypted. -- For document sharing user has inbox space, that can be accessed from outside. Another user can write the document he -wants to share into users' inbox space using the recipients' public key so that only inbox owner can read it. -- For storage systems that do not support file versioning natively (i.e. minio) this library provides versioning -capability too. +Datasafe is a powerful library designed for encrypted and versioned storage of application files, providing an added layer of security to mission-critical, data sensitive applications. -Details about used encryption algorithms can be found in [security whitepaper](SECURITY.WHITEPAPER.md). +Key features of Datasafe include: + +- Unique encryption keys per user, minimizing the risk of mass data breaches. +- File path encryption for complete confidentiality of user files. +- Versioned storage of application files, offering protection against ransomware attacks. +- Asynchronous per-user inboxes for secure file exchange among users. + +## Technical Information +Datasafe employs the AES-GCM algorithm for data encryption and uses CMS-envelopes ([RFC 5652](https://www.rfc-editor.org/rfc/rfc8933#RFC5652)) as an encrypted content wrapper. + +The library is highly configurable, leveraging Dagger2 for dependency injection and a modular architecture that allows for seamless integration into the business layer. This flexibility enables developers to customize various aspects, such as changing the encryption algorithm or disabling path encryption. Each module is designed for maximum independence and can be used separately if needed. -## Features +Datasafe supports various storage options, including Amazon S3, Minio, and local filesystems, with the appropriate adapter. + +In each user's private space, both the document and its path are encrypted. A user can write a document to the recipient's inbox space using the recipients' public key, ensuring that only the intended recipient can read a document. + +For storage systems lacking native file versioning support (e.g. simple file system), Datasafe provides an application layer versioning capability. + +Details about used encryption algorithms can be found in [security whitepaper](SECURITY.WHITEPAPER.md). -- Proprietary software **friendly license** +## Technical Features - **Flexibility** - you can easily change encryption and configure or customize other aspects of library - AES encryption using **CMS-envelopes** for increased security and interoperability with other languages - Secure file sharing with other users - **Extra protection layer** - encryption using securely generated keys that are completely unrelated to your password -- **Client side encryption** - you own your data +- **Application side encryption** - storage layer does not see plain text data - Works with filesystem and Amazon S3 compatible storage - S3, minio, CEPH, etc. -- File names are encrypted -- Thorough testing +- Encrypted file names and file paths +- Thorough application logic and performance testing ## Performance -Datasafe was tested for performance in Amazon cloud. -In short, on m5.xlarge amazon instance with Datasafe library can have write throughput of 50 MiB/s and 80 MiB/s of -read throughput, when using **Amazon S3 bucket** as backing storage (performance is CPU-bound and network-bound). +Datasafe was tested for performance on the AWS. +In short, an m5.xlarge AWS instance with the Datasafe library can have write throughput of 50 MiB/s +and a read throughput 80 MiB/s of, when using **Amazon S3 bucket** as backing storage (performance is CPU-bound and network-bound). Detailed performance report is here: [Datasafe performance results](datasafe-long-run-tests/README.md) @@ -143,12 +143,12 @@ cat private/encrypted_file_name_from_above ![list_actions](docs/demo/list_actions.gif) -### REST based demo +### REST API demo [Here](datasafe-rest-impl/DEMO.md) you can find quick docker-based demo of project capabilities with instructions of how to use it (REST-api based to show how to deploy as encryption server). -## Building project +## Building the Project Without tests: ```bash mvn clean install -DskipTests=true @@ -158,7 +158,7 @@ Full build: mvn clean install ``` -## Adding to your project +## Adding to your Project Datasafe is available from maven-central repository, you can add it to your project using: ```xml @@ -213,12 +213,12 @@ inbox and private space virtual folders - you get similar actions available from Additionally, for file versioning purposes like reading only last file version, there is [versioned privatespace](datasafe-business/src/main/java/de/adorsys/datasafe/business/impl/service/VersionedDatasafeServices.java) that supports versioned and encrypted private file storage (for storage providers that do not support versioning). -# How it works +# How it Works -## Library modules +## Library Modules ![Modules map](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/modules_map.puml&fmt=svg&vvv=1&sanitize=true) -## Users' files - where are they? +## Users' Files - where are they? Whenever user wants to store or read file at some location - be it inbox or his private space, following things do happen: 1. System resolves his profile location @@ -238,7 +238,7 @@ additionally encrypted. ![Path resolution](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/profiles/locate_profile.puml&fmt=svg&vvv=2&sanitize=true) -## Storing private files +## Storing Private Files Private files are always encrypted using users' secret symmetric key. Additionally their path is encrypted too, but this encryption is very special in the sense that it has form of a/b/c encrypted as @@ -252,7 +252,7 @@ encrypted(a)/encrypted(b)/encrypted(c), so that folder traversal operations are [Details](datasafe-privatestore) -## Sharing files with another user +## Sharing files with another User Shared files are protected using asymmetrical cryptography, so that sender encrypts file with recipients' public key and only recipient can read it using his private key. Paths are kept unencrypted for inbox. @@ -265,7 +265,7 @@ and only recipient can read it using his private key. Paths are kept unencrypted [Details](datasafe-inbox) -# Examples of how to use the library +# Examples of how to Use the Library -## Generic Datasafe usage +## Generic Datasafe Usage First, you want to create Datasafe services. This snippet provides you Datasafe that uses filesystem storage adapter: [Example:Create Datasafe services](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithDefaultDatasafeTest.java#L46-L52) ```groovy @@ -400,7 +400,7 @@ assertThat(defaultDatasafeServices.inboxService().read( ).hasContent("shared message"); ``` -## Datasafe with file versioning +## Datasafe with file Versioning Suppose we need to preserve file history, so accidental file removal won't destroy everything. In such case we can use storage provider that supports versioning. But if we have storage provider does not support versions (i.e. minio) we can turn-on software versioning, here is its usage examples; @@ -509,7 +509,7 @@ Instant savedOnPC = versionedServices.latestPrivate() assertThat(savedOnPC).isAfter(savedOnMobile); ``` -## Datasafe on versioned storage +## Datasafe on Versioned Storage If you have storage for user files on **versioned S3 bucket** and want to get object version when you write object or to read some older version encrypted object, you can follow this example of how to do that: [Example:Versioned storage support - writing file and reading back](datasafe-examples/datasafe-examples-versioned-s3/src/test/java/de/adorsys/datasafe/examples/business/s3/BaseUserOperationsWithDefaultDatasafeOnVersionedStorageTest.java#L138-L172) @@ -580,13 +580,13 @@ assertThat(defaultDatasafeServices.privateService().read( ).hasContent("Hello 2"); ``` -## Overriding Datasafe functionality +## Overriding Datasafe Functionality Whenever you want to have some custom functionality of Datasafe, instead of default ones, there are two possible ways to achieve this: - using OverridesRegistry without project recompilation. - using Dagger2 to build a customized version of Datasafe. -### Overriding functionality without recompilation +### Overriding Functionality without Recompilation This approach is for classes annotated with [@RuntimeDelegate](datasafe-types-api/src/main/java/de/adorsys/datasafe/types/api/context/annotations/RuntimeDelegate.java) and it works by putting the custom implementation of a class to be overridden into @@ -619,7 +619,7 @@ datasafeServices.privateService().write(WriteRequest.forDefaultPrivate(user, "fi assertThat(Files.walk(root)).asString().contains("file.txt"); ``` -### Overriding functionality by building custom Datasafe library +### Overriding Functionality by Building custom Datasafe Library This is actually the preferred way to override something or to customize Datasafe. It has no limitations because you can compose any Datasafe service you want using Dagger2 for dependency injection. Its major drawback is that you need to add a dependency to Dagger2 into your project and compile this custom library version. Because of @@ -645,7 +645,7 @@ datasafeServices.privateService().write(WriteRequest.forDefaultPrivate(user, "fi assertThat(walk(root)).asString().contains("file.txt"); ``` -### Customizing Datasafe to store dynamic and user-provided credentials +### Customizing Datasafe to store dynamic and user-provided Credentials In case user wants to register storage credentials himself or place keystore within credentials-protected location one can use this example: [Example:Datasafe with multi-dfs setup](datasafe-examples/datasafe-examples-multidfs/src/test/java/de/adorsys/datasafe/examples/business/s3/MultiDfsWithCredentialsExampleTest.java#L106-L220) @@ -778,7 +778,7 @@ You can read JavaDoc [here](https://adorsys.github.io/datasafe/javadoc/latest/in # FAQ on Licensing -## What is a dual-licensing model? +## What is a Dual-Licensing Model? Under a dual-licensing model, our product is available under two licenses - [The Affero GNU General Public License v3 (AGPL v3)](https://www.gnu.org/licenses/agpl-3.0.en.html) - A proprietary commercial license From 947f38f2ee23cb33a29cacc9a7047c343d4b1309 Mon Sep 17 00:00:00 2001 From: Francis Pouatcha Date: Sun, 30 Apr 2023 20:18:32 -0400 Subject: [PATCH 04/19] Drawing deployment models of datasafe --- README.md | 20 +++++++++++--------- docs/demo/deployment-model.png | Bin 0 -> 74967 bytes 2 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 docs/demo/deployment-model.png diff --git a/README.md b/README.md index e75af9228..44e580f72 100644 --- a/README.md +++ b/README.md @@ -13,28 +13,30 @@ Key features of Datasafe include: - Asynchronous per-user inboxes for secure file exchange among users. ## Technical Information -Datasafe employs the AES-GCM algorithm for data encryption and uses CMS-envelopes ([RFC 5652](https://www.rfc-editor.org/rfc/rfc8933#RFC5652)) as an encrypted content wrapper. +Datasafe employs the AES-GCM algorithm for data encryption and uses CMS-envelopes ([RFC 5652](https://www.rfc-editor.org/rfc/rfc8933#RFC5652)) as an encrypted content wrapper. More details on used encryption algorithms can be found in the [security whitepaper](SECURITY.WHITEPAPER.md). The library is highly configurable, leveraging Dagger2 for dependency injection and a modular architecture that allows for seamless integration into the business layer. This flexibility enables developers to customize various aspects, such as changing the encryption algorithm or disabling path encryption. Each module is designed for maximum independence and can be used separately if needed. Datasafe supports various storage options, including Amazon S3, Minio, and local filesystems, with the appropriate adapter. -In each user's private space, both the document and its path are encrypted. A user can write a document to the recipient's inbox space using the recipients' public key, ensuring that only the intended recipient can read a document. +In each user's private space, both the document and its path are encrypted. A user can write a document to the recipient's inbox space using the recipient's public key, ensuring that only the intended recipient can read a document. For storage systems lacking native file versioning support (e.g. simple file system), Datasafe provides an application layer versioning capability. -Details about used encryption algorithms can be found in [security whitepaper](SECURITY.WHITEPAPER.md). - ## Technical Features -- **Flexibility** - you can easily change encryption and configure or customize other aspects of library -- AES encryption using **CMS-envelopes** for increased security and interoperability with other languages +- Flexibility - you can easily change encryption and configure or customize other aspects of library +- AES encryption using CMS-envelopes for increased security and interoperability with other languages - Secure file sharing with other users -- **Extra protection layer** - encryption using securely generated keys that are completely unrelated to your password -- **Application side encryption** - storage layer does not see plain text data +- Extra protection layer - encryption using securely generated keys that are completely unrelated to your password +- Application side encryption - storage layer does not see plain text data - Works with filesystem and Amazon S3 compatible storage - S3, minio, CEPH, etc. - Encrypted file names and file paths - Thorough application logic and performance testing +## Deployment Model +Followings are among others possible deployment model of the datasafe application. +![Datasafe deployment model](./docs/demo/deployment-model.png) + ## Performance Datasafe was tested for performance on the AWS. @@ -254,7 +256,7 @@ encrypted(a)/encrypted(b)/encrypted(c), so that folder traversal operations are ## Sharing files with another User -Shared files are protected using asymmetrical cryptography, so that sender encrypts file with recipients' public key +Shared files are protected using asymmetrical cryptography, so that sender encrypts file with recipient's public key and only recipient can read it using his private key. Paths are kept unencrypted for inbox. ![How inbox diagram](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/high-level/how_it_works_inbox.puml&fmt=svg&vvv=1&sanitize=true) diff --git a/docs/demo/deployment-model.png b/docs/demo/deployment-model.png new file mode 100644 index 0000000000000000000000000000000000000000..485fd49c58b7162b00f00b405a0308017c9b0fab GIT binary patch literal 74967 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

Zmf?)*~8nv@4q7`Q)y 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 Date: Tue, 31 Oct 2023 11:09:29 +0000 Subject: [PATCH 05/19] Updated keymanagement to version 0.0.9 Increased version to 2.0.2 --- datasafe-business/pom.xml | 2 +- datasafe-cli/pom.xml | 2 +- datasafe-directory/datasafe-directory-api/pom.xml | 2 +- datasafe-directory/datasafe-directory-impl/pom.xml | 2 +- datasafe-directory/pom.xml | 2 +- datasafe-encryption/datasafe-encryption-api/pom.xml | 2 +- datasafe-encryption/datasafe-encryption-impl/pom.xml | 2 +- datasafe-encryption/pom.xml | 2 +- datasafe-examples/datasafe-examples-business/pom.xml | 2 +- datasafe-examples/datasafe-examples-customize-dagger/pom.xml | 2 +- datasafe-examples/datasafe-examples-multidfs/pom.xml | 2 +- datasafe-examples/datasafe-examples-versioned-s3/pom.xml | 2 +- datasafe-examples/pom.xml | 2 +- datasafe-inbox/datasafe-inbox-api/pom.xml | 2 +- datasafe-inbox/datasafe-inbox-impl/pom.xml | 2 +- datasafe-inbox/pom.xml | 2 +- .../datasafe-business-tests-random-actions/pom.xml | 2 +- datasafe-long-run-tests/pom.xml | 2 +- datasafe-metainfo/datasafe-metainfo-version-api/pom.xml | 2 +- datasafe-metainfo/datasafe-metainfo-version-impl/pom.xml | 2 +- datasafe-metainfo/pom.xml | 2 +- datasafe-privatestore/datasafe-privatestore-api/pom.xml | 2 +- datasafe-privatestore/datasafe-privatestore-impl/pom.xml | 2 +- datasafe-privatestore/pom.xml | 2 +- datasafe-rest-impl/pom.xml | 4 ++-- datasafe-runtime-delegate/pom.xml | 2 +- datasafe-simple-adapter/datasafe-simple-adapter-api/pom.xml | 2 +- datasafe-simple-adapter/datasafe-simple-adapter-impl/pom.xml | 2 +- .../datasafe-simple-adapter-spring/pom.xml | 2 +- datasafe-simple-adapter/pom.xml | 2 +- datasafe-storage/datasafe-storage-api/pom.xml | 2 +- datasafe-storage/datasafe-storage-impl-db/pom.xml | 2 +- datasafe-storage/datasafe-storage-impl-fs/pom.xml | 2 +- datasafe-storage/datasafe-storage-impl-s3/pom.xml | 2 +- datasafe-storage/pom.xml | 2 +- datasafe-test-storages/pom.xml | 2 +- datasafe-types-api/pom.xml | 2 +- last-module-codecoverage-check/pom.xml | 2 +- pom.xml | 4 ++-- 39 files changed, 41 insertions(+), 41 deletions(-) diff --git a/datasafe-business/pom.xml b/datasafe-business/pom.xml index 77f3fc705..0b560880e 100644 --- a/datasafe-business/pom.xml +++ b/datasafe-business/pom.xml @@ -5,7 +5,7 @@ datasafe de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-cli/pom.xml b/datasafe-cli/pom.xml index 1475c1449..a185dc1ad 100644 --- a/datasafe-cli/pom.xml +++ b/datasafe-cli/pom.xml @@ -5,7 +5,7 @@ de.adorsys datasafe - 2.0.1-SNAPSHOT + 2.0.2 datasafe-cli diff --git a/datasafe-directory/datasafe-directory-api/pom.xml b/datasafe-directory/datasafe-directory-api/pom.xml index e24d8ed7c..64078b587 100644 --- a/datasafe-directory/datasafe-directory-api/pom.xml +++ b/datasafe-directory/datasafe-directory-api/pom.xml @@ -3,7 +3,7 @@ de.adorsys datasafe-directory - 2.0.1-SNAPSHOT + 2.0.2 datasafe-directory-api diff --git a/datasafe-directory/datasafe-directory-impl/pom.xml b/datasafe-directory/datasafe-directory-impl/pom.xml index 072b6e852..8d01a516a 100644 --- a/datasafe-directory/datasafe-directory-impl/pom.xml +++ b/datasafe-directory/datasafe-directory-impl/pom.xml @@ -3,7 +3,7 @@ de.adorsys datasafe-directory - 2.0.1-SNAPSHOT + 2.0.2 datasafe-directory-impl diff --git a/datasafe-directory/pom.xml b/datasafe-directory/pom.xml index 0f8669f57..7bbf7574f 100644 --- a/datasafe-directory/pom.xml +++ b/datasafe-directory/pom.xml @@ -5,7 +5,7 @@ datasafe de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-encryption/datasafe-encryption-api/pom.xml b/datasafe-encryption/datasafe-encryption-api/pom.xml index 6bc3dc850..4e453c981 100644 --- a/datasafe-encryption/datasafe-encryption-api/pom.xml +++ b/datasafe-encryption/datasafe-encryption-api/pom.xml @@ -5,7 +5,7 @@ de.adorsys datasafe-encryption - 2.0.1-SNAPSHOT + 2.0.2 datasafe-encryption-api diff --git a/datasafe-encryption/datasafe-encryption-impl/pom.xml b/datasafe-encryption/datasafe-encryption-impl/pom.xml index f93d60838..7109cd9b4 100644 --- a/datasafe-encryption/datasafe-encryption-impl/pom.xml +++ b/datasafe-encryption/datasafe-encryption-impl/pom.xml @@ -5,7 +5,7 @@ de.adorsys datasafe-encryption - 2.0.1-SNAPSHOT + 2.0.2 datasafe-encryption-impl diff --git a/datasafe-encryption/pom.xml b/datasafe-encryption/pom.xml index 80172392b..e8597d40f 100644 --- a/datasafe-encryption/pom.xml +++ b/datasafe-encryption/pom.xml @@ -5,7 +5,7 @@ datasafe de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-examples/datasafe-examples-business/pom.xml b/datasafe-examples/datasafe-examples-business/pom.xml index bad2ba76c..6d9e1e396 100644 --- a/datasafe-examples/datasafe-examples-business/pom.xml +++ b/datasafe-examples/datasafe-examples-business/pom.xml @@ -5,7 +5,7 @@ de.adorsys datasafe-examples - 2.0.1-SNAPSHOT + 2.0.2 datasafe-examples-business diff --git a/datasafe-examples/datasafe-examples-customize-dagger/pom.xml b/datasafe-examples/datasafe-examples-customize-dagger/pom.xml index 0a86d584b..bd2050378 100644 --- a/datasafe-examples/datasafe-examples-customize-dagger/pom.xml +++ b/datasafe-examples/datasafe-examples-customize-dagger/pom.xml @@ -5,7 +5,7 @@ de.adorsys datasafe-examples - 2.0.1-SNAPSHOT + 2.0.2 datasafe-examples-customize-dagger diff --git a/datasafe-examples/datasafe-examples-multidfs/pom.xml b/datasafe-examples/datasafe-examples-multidfs/pom.xml index 7a9ef6e4c..1693f8ab1 100644 --- a/datasafe-examples/datasafe-examples-multidfs/pom.xml +++ b/datasafe-examples/datasafe-examples-multidfs/pom.xml @@ -5,7 +5,7 @@ de.adorsys datasafe-examples - 2.0.1-SNAPSHOT + 2.0.2 datasafe-examples-multidfs diff --git a/datasafe-examples/datasafe-examples-versioned-s3/pom.xml b/datasafe-examples/datasafe-examples-versioned-s3/pom.xml index cf14cf1da..fe1bd76db 100644 --- a/datasafe-examples/datasafe-examples-versioned-s3/pom.xml +++ b/datasafe-examples/datasafe-examples-versioned-s3/pom.xml @@ -5,7 +5,7 @@ de.adorsys datasafe-examples - 2.0.1-SNAPSHOT + 2.0.2 datasafe-examples-versioned-s3 diff --git a/datasafe-examples/pom.xml b/datasafe-examples/pom.xml index 2873df125..98d612712 100644 --- a/datasafe-examples/pom.xml +++ b/datasafe-examples/pom.xml @@ -5,7 +5,7 @@ datasafe de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-inbox/datasafe-inbox-api/pom.xml b/datasafe-inbox/datasafe-inbox-api/pom.xml index 84a7fa66c..bd7523ee2 100644 --- a/datasafe-inbox/datasafe-inbox-api/pom.xml +++ b/datasafe-inbox/datasafe-inbox-api/pom.xml @@ -5,7 +5,7 @@ de.adorsys datasafe-inbox - 2.0.1-SNAPSHOT + 2.0.2 datasafe-inbox-api diff --git a/datasafe-inbox/datasafe-inbox-impl/pom.xml b/datasafe-inbox/datasafe-inbox-impl/pom.xml index f1b68c166..19feb8c4d 100644 --- a/datasafe-inbox/datasafe-inbox-impl/pom.xml +++ b/datasafe-inbox/datasafe-inbox-impl/pom.xml @@ -5,7 +5,7 @@ de.adorsys datasafe-inbox - 2.0.1-SNAPSHOT + 2.0.2 datasafe-inbox-impl diff --git a/datasafe-inbox/pom.xml b/datasafe-inbox/pom.xml index 093fcb2c6..84bdb7ab9 100644 --- a/datasafe-inbox/pom.xml +++ b/datasafe-inbox/pom.xml @@ -5,7 +5,7 @@ datasafe de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-long-run-tests/datasafe-business-tests-random-actions/pom.xml b/datasafe-long-run-tests/datasafe-business-tests-random-actions/pom.xml index 89c5456a5..e65650ee9 100644 --- a/datasafe-long-run-tests/datasafe-business-tests-random-actions/pom.xml +++ b/datasafe-long-run-tests/datasafe-business-tests-random-actions/pom.xml @@ -5,7 +5,7 @@ datasafe-long-run-tests de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-long-run-tests/pom.xml b/datasafe-long-run-tests/pom.xml index a550f9048..71593ffc8 100644 --- a/datasafe-long-run-tests/pom.xml +++ b/datasafe-long-run-tests/pom.xml @@ -5,7 +5,7 @@ datasafe de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-metainfo/datasafe-metainfo-version-api/pom.xml b/datasafe-metainfo/datasafe-metainfo-version-api/pom.xml index 98e102c38..e0bc9924f 100644 --- a/datasafe-metainfo/datasafe-metainfo-version-api/pom.xml +++ b/datasafe-metainfo/datasafe-metainfo-version-api/pom.xml @@ -5,7 +5,7 @@ de.adorsys datasafe-metainfo - 2.0.1-SNAPSHOT + 2.0.2 datasafe-metainfo-version-api diff --git a/datasafe-metainfo/datasafe-metainfo-version-impl/pom.xml b/datasafe-metainfo/datasafe-metainfo-version-impl/pom.xml index bafb3174f..d07acfb1f 100644 --- a/datasafe-metainfo/datasafe-metainfo-version-impl/pom.xml +++ b/datasafe-metainfo/datasafe-metainfo-version-impl/pom.xml @@ -5,7 +5,7 @@ de.adorsys datasafe-metainfo - 2.0.1-SNAPSHOT + 2.0.2 datasafe-metainfo-version-impl diff --git a/datasafe-metainfo/pom.xml b/datasafe-metainfo/pom.xml index 17ed12b50..9811a2401 100644 --- a/datasafe-metainfo/pom.xml +++ b/datasafe-metainfo/pom.xml @@ -5,7 +5,7 @@ datasafe de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-privatestore/datasafe-privatestore-api/pom.xml b/datasafe-privatestore/datasafe-privatestore-api/pom.xml index 4ca010e4a..662e128c0 100644 --- a/datasafe-privatestore/datasafe-privatestore-api/pom.xml +++ b/datasafe-privatestore/datasafe-privatestore-api/pom.xml @@ -5,7 +5,7 @@ datasafe-privatestore de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-privatestore/datasafe-privatestore-impl/pom.xml b/datasafe-privatestore/datasafe-privatestore-impl/pom.xml index cb0a4d431..1ba432ee7 100644 --- a/datasafe-privatestore/datasafe-privatestore-impl/pom.xml +++ b/datasafe-privatestore/datasafe-privatestore-impl/pom.xml @@ -5,7 +5,7 @@ datasafe-privatestore de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-privatestore/pom.xml b/datasafe-privatestore/pom.xml index 50494d46c..56ac365c5 100644 --- a/datasafe-privatestore/pom.xml +++ b/datasafe-privatestore/pom.xml @@ -5,7 +5,7 @@ datasafe de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-rest-impl/pom.xml b/datasafe-rest-impl/pom.xml index 4ae567806..fe3dc62b6 100644 --- a/datasafe-rest-impl/pom.xml +++ b/datasafe-rest-impl/pom.xml @@ -5,11 +5,11 @@ datasafe de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 datasafe-rest-impl - 2.0.1-SNAPSHOT + 2.0.2 datasafe-rest-impl Spring Boot DataSafe Application diff --git a/datasafe-runtime-delegate/pom.xml b/datasafe-runtime-delegate/pom.xml index 37aace1bd..f1e6e5d9c 100644 --- a/datasafe-runtime-delegate/pom.xml +++ b/datasafe-runtime-delegate/pom.xml @@ -5,7 +5,7 @@ de.adorsys datasafe - 2.0.1-SNAPSHOT + 2.0.2 datasafe-runtime-delegate diff --git a/datasafe-simple-adapter/datasafe-simple-adapter-api/pom.xml b/datasafe-simple-adapter/datasafe-simple-adapter-api/pom.xml index 12b256919..af5ce5a90 100644 --- a/datasafe-simple-adapter/datasafe-simple-adapter-api/pom.xml +++ b/datasafe-simple-adapter/datasafe-simple-adapter-api/pom.xml @@ -5,7 +5,7 @@ datasafe-simple-adapter de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-simple-adapter/datasafe-simple-adapter-impl/pom.xml b/datasafe-simple-adapter/datasafe-simple-adapter-impl/pom.xml index 36a9b1e4e..b75b2a192 100644 --- a/datasafe-simple-adapter/datasafe-simple-adapter-impl/pom.xml +++ b/datasafe-simple-adapter/datasafe-simple-adapter-impl/pom.xml @@ -5,7 +5,7 @@ datasafe-simple-adapter de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-simple-adapter/datasafe-simple-adapter-spring/pom.xml b/datasafe-simple-adapter/datasafe-simple-adapter-spring/pom.xml index ffd88c2f3..e7e6b562b 100644 --- a/datasafe-simple-adapter/datasafe-simple-adapter-spring/pom.xml +++ b/datasafe-simple-adapter/datasafe-simple-adapter-spring/pom.xml @@ -5,7 +5,7 @@ datasafe-simple-adapter de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-simple-adapter/pom.xml b/datasafe-simple-adapter/pom.xml index 4c8fe5f8f..1db1b4c3c 100644 --- a/datasafe-simple-adapter/pom.xml +++ b/datasafe-simple-adapter/pom.xml @@ -5,7 +5,7 @@ datasafe de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-storage/datasafe-storage-api/pom.xml b/datasafe-storage/datasafe-storage-api/pom.xml index fa57d90aa..640b8bb51 100644 --- a/datasafe-storage/datasafe-storage-api/pom.xml +++ b/datasafe-storage/datasafe-storage-api/pom.xml @@ -5,7 +5,7 @@ de.adorsys datasafe-storage - 2.0.1-SNAPSHOT + 2.0.2 datasafe-storage-api diff --git a/datasafe-storage/datasafe-storage-impl-db/pom.xml b/datasafe-storage/datasafe-storage-impl-db/pom.xml index aed79cfa5..b54ca648e 100644 --- a/datasafe-storage/datasafe-storage-impl-db/pom.xml +++ b/datasafe-storage/datasafe-storage-impl-db/pom.xml @@ -5,7 +5,7 @@ datasafe-storage de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-storage/datasafe-storage-impl-fs/pom.xml b/datasafe-storage/datasafe-storage-impl-fs/pom.xml index 386f41627..5c38d1b0a 100644 --- a/datasafe-storage/datasafe-storage-impl-fs/pom.xml +++ b/datasafe-storage/datasafe-storage-impl-fs/pom.xml @@ -5,7 +5,7 @@ datasafe-storage de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-storage/datasafe-storage-impl-s3/pom.xml b/datasafe-storage/datasafe-storage-impl-s3/pom.xml index 28815f0f8..c9bd41a17 100644 --- a/datasafe-storage/datasafe-storage-impl-s3/pom.xml +++ b/datasafe-storage/datasafe-storage-impl-s3/pom.xml @@ -5,7 +5,7 @@ datasafe-storage de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-storage/pom.xml b/datasafe-storage/pom.xml index 1158c9bb8..70881ff1e 100644 --- a/datasafe-storage/pom.xml +++ b/datasafe-storage/pom.xml @@ -5,7 +5,7 @@ datasafe de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-test-storages/pom.xml b/datasafe-test-storages/pom.xml index 2542f9977..e0364c713 100644 --- a/datasafe-test-storages/pom.xml +++ b/datasafe-test-storages/pom.xml @@ -5,7 +5,7 @@ datasafe de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/datasafe-types-api/pom.xml b/datasafe-types-api/pom.xml index 52c18bd1a..97ceaa5d9 100644 --- a/datasafe-types-api/pom.xml +++ b/datasafe-types-api/pom.xml @@ -5,7 +5,7 @@ de.adorsys datasafe - 2.0.1-SNAPSHOT + 2.0.2 datasafe-types-api diff --git a/last-module-codecoverage-check/pom.xml b/last-module-codecoverage-check/pom.xml index 165631c42..a45712d8f 100644 --- a/last-module-codecoverage-check/pom.xml +++ b/last-module-codecoverage-check/pom.xml @@ -5,7 +5,7 @@ datasafe de.adorsys - 2.0.1-SNAPSHOT + 2.0.2 4.0.0 diff --git a/pom.xml b/pom.xml index 55001eb29..07f758810 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ de.adorsys datasafe - 2.0.1-SNAPSHOT + 2.0.2 datasafe Datasafe https://github.com/adorsys/datasafe @@ -119,7 +119,7 @@ 1.4.4 2.12.7 2.12.7.1 - 0.0.7 + 0.0.9 2.1.1 2.3.1 From 9dad6216f971e57a1984c0c52fab9d3ca7c83664 Mon Sep 17 00:00:00 2001 From: marcelmeyer Date: Tue, 31 Oct 2023 12:54:20 +0000 Subject: [PATCH 06/19] Updated keymanagement to version 0.0.9 Increased version to 2.0.2 --- pom.xml | 51 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/pom.xml b/pom.xml index 07f758810..eb2583f13 100644 --- a/pom.xml +++ b/pom.xml @@ -537,23 +537,23 @@ - - org.projectlombok - lombok-maven-plugin - ${lombok-maven-plugin.version} - - - generate-sources - - delombok - - - false - src/main/java - - - - + + + + + + + + + + + + + + + + + org.apache.maven.plugins maven-javadoc-plugin @@ -574,12 +574,14 @@ org.sonatype.plugins nexus-staging-maven-plugin - 1.6.8 + 1.6.13 true sonatype https://oss.sonatype.org/ true + true + 30 @@ -598,8 +600,13 @@ org.apache.maven.plugins maven-gpg-plugin - 1.6 + 3.0.1 + opensource@adorsys.de + + --pinentry-mode + loopback + @@ -608,6 +615,12 @@ sign + + + --pinentry-mode + loopback + + From 741c1f306a39c3a5beee6b15491140fa8a7d3a35 Mon Sep 17 00:00:00 2001 From: Maxim Grischenko Date: Tue, 14 Nov 2023 15:12:12 +0100 Subject: [PATCH 07/19] fix javadoc --- datasafe-business/pom.xml | 1 + .../CMSEncryptionServiceImpl.java | 2 +- .../CustomlyBuiltDatasafeServices.java | 2 +- .../pom.xml | 1 + .../datasafe-simple-adapter-impl/pom.xml | 1 + .../impl/SimpleDatasafeAdapterTest.java | 5 -- .../datasafe-simple-adapter-spring/pom.xml | 1 + .../simple/adapter/spring/InjectionTest.java | 1 - ...ithoutPathEncryptionForFilesystemTest.java | 2 - datasafe-test-storages/pom.xml | 18 ++++++ .../teststorage/WithStorageProvider.java | 1 - pom.xml | 57 +++++++++---------- 12 files changed, 51 insertions(+), 41 deletions(-) rename datasafe-test-storages/src/{main => test}/java/de/adorsys/datasafe/teststorage/WithStorageProvider.java (99%) diff --git a/datasafe-business/pom.xml b/datasafe-business/pom.xml index 0b560880e..4c814b1ec 100644 --- a/datasafe-business/pom.xml +++ b/datasafe-business/pom.xml @@ -113,6 +113,7 @@ de.adorsys datasafe-test-storages ${project.version} + test-jar test diff --git a/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/cmsencryption/CMSEncryptionServiceImpl.java b/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/cmsencryption/CMSEncryptionServiceImpl.java index 4d8dc886d..abca2737d 100644 --- a/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/cmsencryption/CMSEncryptionServiceImpl.java +++ b/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/cmsencryption/CMSEncryptionServiceImpl.java @@ -36,7 +36,7 @@ /** * Cryptographic message syntax document encoder/decoder - see * - * @see CMS + * @see CMS * wiki */ @Slf4j diff --git a/datasafe-examples/datasafe-examples-customize-dagger/src/main/java/de/adorsys/datasafe/examples/business/filesystem/CustomlyBuiltDatasafeServices.java b/datasafe-examples/datasafe-examples-customize-dagger/src/main/java/de/adorsys/datasafe/examples/business/filesystem/CustomlyBuiltDatasafeServices.java index 898b265c7..215083b39 100644 --- a/datasafe-examples/datasafe-examples-customize-dagger/src/main/java/de/adorsys/datasafe/examples/business/filesystem/CustomlyBuiltDatasafeServices.java +++ b/datasafe-examples/datasafe-examples-customize-dagger/src/main/java/de/adorsys/datasafe/examples/business/filesystem/CustomlyBuiltDatasafeServices.java @@ -23,7 +23,7 @@ /** * This is Datasafe services customized implementation. - * Note, that despite is has {@code @Singleton} annotation, it is not real singleton, the only shared thing + * Note, that despite it has {@code @Singleton} annotation, it is not real singleton, the only shared thing * across all services instantiated using build() is bindings with {@code Singleton} in its Module. */ @Singleton diff --git a/datasafe-long-run-tests/datasafe-business-tests-random-actions/pom.xml b/datasafe-long-run-tests/datasafe-business-tests-random-actions/pom.xml index e65650ee9..c272f303f 100644 --- a/datasafe-long-run-tests/datasafe-business-tests-random-actions/pom.xml +++ b/datasafe-long-run-tests/datasafe-business-tests-random-actions/pom.xml @@ -110,6 +110,7 @@ de.adorsys datasafe-test-storages ${project.version} + test-jar test diff --git a/datasafe-simple-adapter/datasafe-simple-adapter-impl/pom.xml b/datasafe-simple-adapter/datasafe-simple-adapter-impl/pom.xml index b75b2a192..d9cf19796 100644 --- a/datasafe-simple-adapter/datasafe-simple-adapter-impl/pom.xml +++ b/datasafe-simple-adapter/datasafe-simple-adapter-impl/pom.xml @@ -45,6 +45,7 @@ de.adorsys datasafe-test-storages ${project.version} + test-jar test diff --git a/datasafe-simple-adapter/datasafe-simple-adapter-impl/src/test/java/de/adorsys/datasafe/simple/adapter/impl/SimpleDatasafeAdapterTest.java b/datasafe-simple-adapter/datasafe-simple-adapter-impl/src/test/java/de/adorsys/datasafe/simple/adapter/impl/SimpleDatasafeAdapterTest.java index a7ce59715..2fb8aa818 100644 --- a/datasafe-simple-adapter/datasafe-simple-adapter-impl/src/test/java/de/adorsys/datasafe/simple/adapter/impl/SimpleDatasafeAdapterTest.java +++ b/datasafe-simple-adapter/datasafe-simple-adapter-impl/src/test/java/de/adorsys/datasafe/simple/adapter/impl/SimpleDatasafeAdapterTest.java @@ -1,7 +1,6 @@ package de.adorsys.datasafe.simple.adapter.impl; import com.amazonaws.services.s3.model.AmazonS3Exception; -import com.sun.management.UnixOperatingSystemMXBean; import de.adorsys.datasafe.encrypiton.api.types.UserID; import de.adorsys.datasafe.encrypiton.api.types.UserIDAuth; import de.adorsys.datasafe.encrypiton.api.types.encryption.MutableEncryptionConfig; @@ -24,17 +23,13 @@ import de.adorsys.datasafe.types.api.utils.ReadKeyPasswordTestFactory; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import net.bytebuddy.implementation.bytecode.Throw; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import java.io.ByteArrayInputStream; import java.io.OutputStream; -import java.lang.management.ManagementFactory; -import java.lang.management.OperatingSystemMXBean; import java.nio.file.NoSuchFileException; import java.security.UnrecoverableKeyException; import java.util.ArrayList; diff --git a/datasafe-simple-adapter/datasafe-simple-adapter-spring/pom.xml b/datasafe-simple-adapter/datasafe-simple-adapter-spring/pom.xml index e7e6b562b..4e1543476 100644 --- a/datasafe-simple-adapter/datasafe-simple-adapter-spring/pom.xml +++ b/datasafe-simple-adapter/datasafe-simple-adapter-spring/pom.xml @@ -126,6 +126,7 @@ de.adorsys datasafe-test-storages ${project.version} + test-jar test diff --git a/datasafe-simple-adapter/datasafe-simple-adapter-spring/src/test/java/de/adorsys/datasafe/simple/adapter/spring/InjectionTest.java b/datasafe-simple-adapter/datasafe-simple-adapter-spring/src/test/java/de/adorsys/datasafe/simple/adapter/spring/InjectionTest.java index e230609f1..e10f39431 100644 --- a/datasafe-simple-adapter/datasafe-simple-adapter-spring/src/test/java/de/adorsys/datasafe/simple/adapter/spring/InjectionTest.java +++ b/datasafe-simple-adapter/datasafe-simple-adapter-spring/src/test/java/de/adorsys/datasafe/simple/adapter/spring/InjectionTest.java @@ -21,7 +21,6 @@ import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.test.context.SpringBootTest; diff --git a/datasafe-simple-adapter/datasafe-simple-adapter-spring/src/test/java/de/adorsys/datasafe/simple/adapter/spring/InjectionWithoutPathEncryptionForFilesystemTest.java b/datasafe-simple-adapter/datasafe-simple-adapter-spring/src/test/java/de/adorsys/datasafe/simple/adapter/spring/InjectionWithoutPathEncryptionForFilesystemTest.java index 969653f13..52b6636ea 100644 --- a/datasafe-simple-adapter/datasafe-simple-adapter-spring/src/test/java/de/adorsys/datasafe/simple/adapter/spring/InjectionWithoutPathEncryptionForFilesystemTest.java +++ b/datasafe-simple-adapter/datasafe-simple-adapter-spring/src/test/java/de/adorsys/datasafe/simple/adapter/spring/InjectionWithoutPathEncryptionForFilesystemTest.java @@ -11,9 +11,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringBootConfiguration; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import java.util.stream.Stream; diff --git a/datasafe-test-storages/pom.xml b/datasafe-test-storages/pom.xml index e0364c713..e1fa0d67b 100644 --- a/datasafe-test-storages/pom.xml +++ b/datasafe-test-storages/pom.xml @@ -73,4 +73,22 @@ + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + test-jar + + test-compile + + + + + diff --git a/datasafe-test-storages/src/main/java/de/adorsys/datasafe/teststorage/WithStorageProvider.java b/datasafe-test-storages/src/test/java/de/adorsys/datasafe/teststorage/WithStorageProvider.java similarity index 99% rename from datasafe-test-storages/src/main/java/de/adorsys/datasafe/teststorage/WithStorageProvider.java rename to datasafe-test-storages/src/test/java/de/adorsys/datasafe/teststorage/WithStorageProvider.java index 2b5684e13..ba03f52c4 100644 --- a/datasafe-test-storages/src/main/java/de/adorsys/datasafe/teststorage/WithStorageProvider.java +++ b/datasafe-test-storages/src/test/java/de/adorsys/datasafe/teststorage/WithStorageProvider.java @@ -18,7 +18,6 @@ import de.adorsys.datasafe.types.api.resource.Uri; import de.adorsys.datasafe.types.api.shared.BaseMockitoTest; import de.adorsys.datasafe.types.api.utils.ExecutorServiceUtil; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.SneakyThrows; import lombok.ToString; diff --git a/pom.xml b/pom.xml index eb2583f13..080eae256 100644 --- a/pom.xml +++ b/pom.xml @@ -83,7 +83,7 @@ 17 17 3.6.0 - 1.18.28 + 1.18.30 2.8.9 2.46.1 32.1.1-jre @@ -371,21 +371,10 @@ ${surefireArgLine}, - --add-opens java.base/java.lang=ALL-UNNAMED, - --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED, --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED, - - --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED, - --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED, - --add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED, --add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED, - --add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED, - --add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED, - --add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED, - --add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED, - --add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED, - --add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED + --add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED @@ -537,23 +526,31 @@ - - - - - - - - - - - - - - - - - + + org.projectlombok + lombok-maven-plugin + ${lombok-maven-plugin.version} + + + org.projectlombok + lombok + ${lombok.version} + + + + + + generate-sources + + delombok + + + false + src/main/java + + + + org.apache.maven.plugins maven-javadoc-plugin From 9f54331b61c967ab897061ccda5661a4e8808661 Mon Sep 17 00:00:00 2001 From: Maxim Grischenko Date: Tue, 3 Oct 2023 12:07:25 +0200 Subject: [PATCH 08/19] during release fixes --- .../impl/cmsencryption/CMSEncryptionServiceImpl.java | 2 +- .../pathencryption/IntegrityPreservingUriEncryption.java | 2 +- pom.xml | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/cmsencryption/CMSEncryptionServiceImpl.java b/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/cmsencryption/CMSEncryptionServiceImpl.java index 4d8dc886d..abca2737d 100644 --- a/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/cmsencryption/CMSEncryptionServiceImpl.java +++ b/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/cmsencryption/CMSEncryptionServiceImpl.java @@ -36,7 +36,7 @@ /** * Cryptographic message syntax document encoder/decoder - see * - * @see CMS + * @see CMS * wiki */ @Slf4j diff --git a/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/pathencryption/IntegrityPreservingUriEncryption.java b/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/pathencryption/IntegrityPreservingUriEncryption.java index 928a358d7..f24d11f7f 100644 --- a/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/pathencryption/IntegrityPreservingUriEncryption.java +++ b/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/pathencryption/IntegrityPreservingUriEncryption.java @@ -24,7 +24,7 @@ * It means that path/to/file is encrypted to cipher(path)/cipher(to)/cipher(file) and each invocation of example: * cipher(path) will yield same string, but cipher(path)/cipher(path) will not yield same segments - * it will be more like abc/cde and not like abc/abc. - * Additionally each segment is authenticated against its parent path hash, so attacker can't + * Additionally, each segment is authenticated against its parent path hash, so attacker can't * move a/file to b/file without being detected. */ @Slf4j diff --git a/pom.xml b/pom.xml index eb2583f13..9e8d51d92 100644 --- a/pom.xml +++ b/pom.xml @@ -600,7 +600,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.0.1 + 3.1.0 opensource@adorsys.de @@ -627,8 +627,9 @@ org.apache.maven.plugins maven-javadoc-plugin + 3.6.0 - -Xdoclint:none + -Xdoclint:none From ecd4c67b203ef6e4cca292c243d4f2ef06f38149 Mon Sep 17 00:00:00 2001 From: Maxim Grischenko Date: Fri, 6 Oct 2023 11:42:08 +0200 Subject: [PATCH 09/19] update plugin versions --- .../pom.xml | 7 +---- last-module-codecoverage-check/pom.xml | 6 +++- pom.xml | 29 ++++++++++++------- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/datasafe-long-run-tests/datasafe-business-tests-random-actions/pom.xml b/datasafe-long-run-tests/datasafe-business-tests-random-actions/pom.xml index e65650ee9..7d8b7c7ee 100644 --- a/datasafe-long-run-tests/datasafe-business-tests-random-actions/pom.xml +++ b/datasafe-long-run-tests/datasafe-business-tests-random-actions/pom.xml @@ -11,10 +11,6 @@ datasafe-business-tests-random-actions - - 2.22.2 - - de.adorsys @@ -185,9 +181,8 @@ org.apache.maven.plugins maven-surefire-plugin - ${maven.surefire.plugin.version} + ${surefire.version} - once ${testArgs} diff --git a/last-module-codecoverage-check/pom.xml b/last-module-codecoverage-check/pom.xml index a45712d8f..fd80ffd30 100644 --- a/last-module-codecoverage-check/pom.xml +++ b/last-module-codecoverage-check/pom.xml @@ -11,6 +11,10 @@ last-module-codecoverage-check + + 3.3.1 + + de.adorsys @@ -144,7 +148,7 @@ maven-resources-plugin - 3.3.1 + ${maven-resources-plugin.version} package diff --git a/pom.xml b/pom.xml index 9e8d51d92..f9ac6d80c 100644 --- a/pom.xml +++ b/pom.xml @@ -91,7 +91,7 @@ 5.10.0 3.12.2 5.5.0 - 2.22.1 + 3.1.2 1.21 UTF-8 false @@ -122,6 +122,15 @@ 0.0.9 2.1.1 2.3.1 + 2.8.1 + 1.3 + 3.3.0 + 2.5.2 + 2.7 + 3.6.0 + 3.1.0 + 3.0.1 + 1.6.13 @@ -574,7 +583,7 @@ org.sonatype.plugins nexus-staging-maven-plugin - 1.6.13 + ${nexus-staging-maven-plugin.version} true sonatype @@ -587,7 +596,7 @@ org.apache.maven.plugins maven-source-plugin - 3.0.1 + ${maven-source-plugin.version} attach-sources @@ -600,7 +609,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.1.0 + ${maven-gpg-plugin.version} opensource@adorsys.de @@ -627,7 +636,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.6.0 + ${maven-javadoc-plugin.version} -Xdoclint:none @@ -647,11 +656,11 @@ org.apache.maven.plugins maven-project-info-reports-plugin - 2.7 + ${maven-project-info-reports-plugin.version} maven-release-plugin - 2.5.2 + ${maven-release-plugin.version} release true @@ -661,7 +670,7 @@ org.apache.maven.plugins maven-jar-plugin - 2.6 + ${maven-jar-plugin.version} @@ -684,7 +693,7 @@ org.codehaus.mojo buildnumber-maven-plugin - 1.3 + ${buildnumber-maven-plugin.version} validate @@ -701,7 +710,7 @@ org.apache.maven.plugins maven-deploy-plugin - 2.8.1 + ${maven-deploy-plugin.version} ${deploy.disabled} From 8bde9ebc4e3bd0eadc8e56c03013606c62a0c94f Mon Sep 17 00:00:00 2001 From: Maxim Grischenko Date: Tue, 14 Nov 2023 16:23:38 +0100 Subject: [PATCH 10/19] add javadoc version --- pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pom.xml b/pom.xml index 080eae256..2df872154 100644 --- a/pom.xml +++ b/pom.xml @@ -122,6 +122,7 @@ 0.0.9 2.1.1 2.3.1 + 3.6.2 @@ -554,6 +555,7 @@ org.apache.maven.plugins maven-javadoc-plugin + ${maven-javadoc-plugin.version} **/*Dagger* From 495d803837986201d4e7b05af0faca84fde06f00 Mon Sep 17 00:00:00 2001 From: Maxim Grischenko Date: Tue, 14 Nov 2023 17:45:17 +0100 Subject: [PATCH 11/19] remove unneeded --add-opens --- pom.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 2df872154..721a668ac 100644 --- a/pom.xml +++ b/pom.xml @@ -374,8 +374,7 @@ ${surefireArgLine}, --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED, --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED, - --add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED, - --add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + --add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED, From 2be11ec2acf70d570cbe76eef75257b6d3a2c811 Mon Sep 17 00:00:00 2001 From: Maxim Grischenko Date: Tue, 3 Oct 2023 12:07:25 +0200 Subject: [PATCH 12/19] release fixes --- .../impl/cmsencryption/CMSEncryptionServiceImpl.java | 2 +- .../pathencryption/IntegrityPreservingUriEncryption.java | 2 +- pom.xml | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/cmsencryption/CMSEncryptionServiceImpl.java b/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/cmsencryption/CMSEncryptionServiceImpl.java index 4d8dc886d..abca2737d 100644 --- a/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/cmsencryption/CMSEncryptionServiceImpl.java +++ b/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/cmsencryption/CMSEncryptionServiceImpl.java @@ -36,7 +36,7 @@ /** * Cryptographic message syntax document encoder/decoder - see * - * @see CMS + * @see CMS * wiki */ @Slf4j diff --git a/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/pathencryption/IntegrityPreservingUriEncryption.java b/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/pathencryption/IntegrityPreservingUriEncryption.java index 928a358d7..f24d11f7f 100644 --- a/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/pathencryption/IntegrityPreservingUriEncryption.java +++ b/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/pathencryption/IntegrityPreservingUriEncryption.java @@ -24,7 +24,7 @@ * It means that path/to/file is encrypted to cipher(path)/cipher(to)/cipher(file) and each invocation of example: * cipher(path) will yield same string, but cipher(path)/cipher(path) will not yield same segments - * it will be more like abc/cde and not like abc/abc. - * Additionally each segment is authenticated against its parent path hash, so attacker can't + * Additionally, each segment is authenticated against its parent path hash, so attacker can't * move a/file to b/file without being detected. */ @Slf4j diff --git a/pom.xml b/pom.xml index eb2583f13..9e8d51d92 100644 --- a/pom.xml +++ b/pom.xml @@ -600,7 +600,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.0.1 + 3.1.0 opensource@adorsys.de @@ -627,8 +627,9 @@ org.apache.maven.plugins maven-javadoc-plugin + 3.6.0 - -Xdoclint:none + -Xdoclint:none From cae3f85d9eb9b5f12e826db8b4fa50707e2e5ca9 Mon Sep 17 00:00:00 2001 From: Maxim Grischenko Date: Fri, 6 Oct 2023 11:42:08 +0200 Subject: [PATCH 13/19] update plugin versions --- .../pom.xml | 7 +---- last-module-codecoverage-check/pom.xml | 6 +++- pom.xml | 29 ++++++++++++------- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/datasafe-long-run-tests/datasafe-business-tests-random-actions/pom.xml b/datasafe-long-run-tests/datasafe-business-tests-random-actions/pom.xml index e65650ee9..7d8b7c7ee 100644 --- a/datasafe-long-run-tests/datasafe-business-tests-random-actions/pom.xml +++ b/datasafe-long-run-tests/datasafe-business-tests-random-actions/pom.xml @@ -11,10 +11,6 @@ datasafe-business-tests-random-actions - - 2.22.2 - - de.adorsys @@ -185,9 +181,8 @@ org.apache.maven.plugins maven-surefire-plugin - ${maven.surefire.plugin.version} + ${surefire.version} - once ${testArgs} diff --git a/last-module-codecoverage-check/pom.xml b/last-module-codecoverage-check/pom.xml index a45712d8f..fd80ffd30 100644 --- a/last-module-codecoverage-check/pom.xml +++ b/last-module-codecoverage-check/pom.xml @@ -11,6 +11,10 @@ last-module-codecoverage-check + + 3.3.1 + + de.adorsys @@ -144,7 +148,7 @@ maven-resources-plugin - 3.3.1 + ${maven-resources-plugin.version} package diff --git a/pom.xml b/pom.xml index 9e8d51d92..f9ac6d80c 100644 --- a/pom.xml +++ b/pom.xml @@ -91,7 +91,7 @@ 5.10.0 3.12.2 5.5.0 - 2.22.1 + 3.1.2 1.21 UTF-8 false @@ -122,6 +122,15 @@ 0.0.9 2.1.1 2.3.1 + 2.8.1 + 1.3 + 3.3.0 + 2.5.2 + 2.7 + 3.6.0 + 3.1.0 + 3.0.1 + 1.6.13 @@ -574,7 +583,7 @@ org.sonatype.plugins nexus-staging-maven-plugin - 1.6.13 + ${nexus-staging-maven-plugin.version} true sonatype @@ -587,7 +596,7 @@ org.apache.maven.plugins maven-source-plugin - 3.0.1 + ${maven-source-plugin.version} attach-sources @@ -600,7 +609,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.1.0 + ${maven-gpg-plugin.version} opensource@adorsys.de @@ -627,7 +636,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.6.0 + ${maven-javadoc-plugin.version} -Xdoclint:none @@ -647,11 +656,11 @@ org.apache.maven.plugins maven-project-info-reports-plugin - 2.7 + ${maven-project-info-reports-plugin.version} maven-release-plugin - 2.5.2 + ${maven-release-plugin.version} release true @@ -661,7 +670,7 @@ org.apache.maven.plugins maven-jar-plugin - 2.6 + ${maven-jar-plugin.version} @@ -684,7 +693,7 @@ org.codehaus.mojo buildnumber-maven-plugin - 1.3 + ${buildnumber-maven-plugin.version} validate @@ -701,7 +710,7 @@ org.apache.maven.plugins maven-deploy-plugin - 2.8.1 + ${maven-deploy-plugin.version} ${deploy.disabled} From e2199b328f51aff9bae9619bf8278285f1f2c531 Mon Sep 17 00:00:00 2001 From: Maxim Grischenko Date: Thu, 26 Oct 2023 19:16:18 +0200 Subject: [PATCH 14/19] use by default ECDH algo for encrypting keys --- .../api/types/encryption/KeyCreationConfig.java | 12 ++++++------ .../src/main/resources/application.properties | 8 ++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/datasafe-encryption/datasafe-encryption-api/src/main/java/de/adorsys/datasafe/encrypiton/api/types/encryption/KeyCreationConfig.java b/datasafe-encryption/datasafe-encryption-api/src/main/java/de/adorsys/datasafe/encrypiton/api/types/encryption/KeyCreationConfig.java index 9bab88b51..1434414bd 100644 --- a/datasafe-encryption/datasafe-encryption-api/src/main/java/de/adorsys/datasafe/encrypiton/api/types/encryption/KeyCreationConfig.java +++ b/datasafe-encryption/datasafe-encryption-api/src/main/java/de/adorsys/datasafe/encrypiton/api/types/encryption/KeyCreationConfig.java @@ -45,13 +45,13 @@ public static class SecretKeyCreationCfg { public static class EncryptingKeyCreationCfg { @Builder.Default - private final String algo = "RSA"; + private final String algo = "ECDH"; @Builder.Default - private final int size = 2048; + private final int size = 256; @Builder.Default - private final String sigAlgo = "SHA256withRSA"; + private final String sigAlgo = "SHA256withECDSA"; } @Getter @@ -59,12 +59,12 @@ public static class EncryptingKeyCreationCfg { public static class SigningKeyCreationCfg { @Builder.Default - private final String algo = "RSA"; + private final String algo = "ECDH"; @Builder.Default - private final int size = 2048; + private final int size = 256; @Builder.Default - private final String sigAlgo = "SHA256withRSA"; + private final String sigAlgo = "SHA256withECDSA"; } } diff --git a/datasafe-rest-impl/src/main/resources/application.properties b/datasafe-rest-impl/src/main/resources/application.properties index 0c7f28841..549395490 100644 --- a/datasafe-rest-impl/src/main/resources/application.properties +++ b/datasafe-rest-impl/src/main/resources/application.properties @@ -36,3 +36,11 @@ datasafe.encryption.keystore.pbkdf.scrypt.parallelization=1 datasafe.encryption.keystore.pbkdf.scrypt.saltLength=16 datasafe.encryption.keystore.macAlgo=HmacSHA3_512 datasafe.encryption.cms.algo=AES256_GCM + +#datasafe.encryption.keys.encrypting.algo=RSA +#datasafe.encryption.keys.encrypting.size=4096 +#datasafe.encryption.keys.encrypting.sigAlgo=SHA256withRSA + +#datasafe.encryption.keys.signing.algo=RSA +#datasafe.encryption.keys.signing.size=4096 +#datasafe.encryption.keys.signing.sigAlgo=SHA256withRSA From a9568ebe46391ffe29adee34c6b5a09dcb97edf2 Mon Sep 17 00:00:00 2001 From: Maxim Grischenko Date: Tue, 28 Nov 2023 12:45:34 +0100 Subject: [PATCH 15/19] use keymanagement 0.0.11-SNAPSHOT, add paramSpec in Encryption/Sighning template --- .../encrypiton/api/types/encryption/KeyCreationConfig.java | 6 ++++++ .../encrypiton/impl/keystore/KeyStoreServiceImpl.java | 3 +++ pom.xml | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/datasafe-encryption/datasafe-encryption-api/src/main/java/de/adorsys/datasafe/encrypiton/api/types/encryption/KeyCreationConfig.java b/datasafe-encryption/datasafe-encryption-api/src/main/java/de/adorsys/datasafe/encrypiton/api/types/encryption/KeyCreationConfig.java index 1434414bd..7d20aaaa2 100644 --- a/datasafe-encryption/datasafe-encryption-api/src/main/java/de/adorsys/datasafe/encrypiton/api/types/encryption/KeyCreationConfig.java +++ b/datasafe-encryption/datasafe-encryption-api/src/main/java/de/adorsys/datasafe/encrypiton/api/types/encryption/KeyCreationConfig.java @@ -52,6 +52,9 @@ public static class EncryptingKeyCreationCfg { @Builder.Default private final String sigAlgo = "SHA256withECDSA"; + + @Builder.Default + private final String customNamedCurve = "Curve25519"; } @Getter @@ -66,5 +69,8 @@ public static class SigningKeyCreationCfg { @Builder.Default private final String sigAlgo = "SHA256withECDSA"; + + @Builder.Default + private final String customNamedCurve = "Curve25519"; } } diff --git a/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/keystore/KeyStoreServiceImpl.java b/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/keystore/KeyStoreServiceImpl.java index fd1487d9f..622a624d9 100644 --- a/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/keystore/KeyStoreServiceImpl.java +++ b/datasafe-encryption/datasafe-encryption-impl/src/main/java/de/adorsys/datasafe/encrypiton/impl/keystore/KeyStoreServiceImpl.java @@ -19,6 +19,8 @@ import de.adorsys.keymanagement.api.types.template.generated.Secret; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.crypto.ec.CustomNamedCurves; +import org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; @@ -89,6 +91,7 @@ public KeyStore createKeyStore(KeyStoreAuth keyStoreAuth, .keySize(encConf.getSize()) .prefix("ENC") .password(passSupplier) + .paramSpec(EC5Util.convertToSpec(CustomNamedCurves.getByName(encConf.getCustomNamedCurve()))) .build() .repeat(keyConfig.getEncKeyNumber()) ) diff --git a/pom.xml b/pom.xml index f9ac6d80c..aa0fdcada 100644 --- a/pom.xml +++ b/pom.xml @@ -119,7 +119,7 @@ 1.4.4 2.12.7 2.12.7.1 - 0.0.9 + 0.0.11-SNAPSHOT 2.1.1 2.3.1 2.8.1 From 374fb085c49455348236f1a5d2898832d6a4927b Mon Sep 17 00:00:00 2001 From: Francis Pouatcha Date: Mon, 11 Dec 2023 19:49:45 -0500 Subject: [PATCH 16/19] Proof reading --- README.md | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 44e580f72..a3ab65ada 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Datasafe supports various storage options, including Amazon S3, Minio, and local In each user's private space, both the document and its path are encrypted. A user can write a document to the recipient's inbox space using the recipient's public key, ensuring that only the intended recipient can read a document. -For storage systems lacking native file versioning support (e.g. simple file system), Datasafe provides an application layer versioning capability. +For storage systems lacking native file versioning support (e.g. simple file system), datasafe provides an application layer versioning capability. ## Technical Features - Flexibility - you can easily change encryption and configure or customize other aspects of library @@ -34,21 +34,18 @@ For storage systems lacking native file versioning support (e.g. simple file sys - Thorough application logic and performance testing ## Deployment Model -Followings are among others possible deployment model of the datasafe application. +Followings are among others possible deployment models of the datasafe application. ![Datasafe deployment model](./docs/demo/deployment-model.png) ## Performance - -Datasafe was tested for performance on the AWS. -In short, an m5.xlarge AWS instance with the Datasafe library can have write throughput of 50 MiB/s -and a read throughput 80 MiB/s of, when using **Amazon S3 bucket** as backing storage (performance is CPU-bound and network-bound). +Datasafe was tested for performance on the AWS. In short, an m5.xlarge AWS instance with the datasafe library can have write throughput of 50 MiB/s and a read throughput 80 MiB/s of, when using **Amazon S3 bucket** as backing storage (performance is CPU-bound and network-bound). Detailed performance report is here: [Datasafe performance results](datasafe-long-run-tests/README.md) ## Quick demo ### Datasafe-CLI -You can try Datasafe as a CLI (command-line-interface) executable for encryption of your own sensitive files. +You can try datasafe as a CLI (command-line-interface) executable for encryption of your own sensitive files. Your encrypted files can be saved either in S3 bucket or local filesystem safely, because encryption will happen locally - on your machine (See [CLI-README](datasafe-cli/README.md) for details). @@ -69,7 +66,7 @@ locally - on your machine (See [CLI-README](datasafe-cli/README.md) for details) ```bash curl -L https://github.com/adorsys/datasafe/releases/download/v0.6.0/datasafe-cli-osx-x64 > datasafe-cli && chmod +x datasafe-cli ``` -- Create file with your credentials (they also can be passed through commandline) +- Create file with your credentials (they also can be passed through command line) ```bash echo '{"username": "john", "password": "Doe", "systemPassword": "password"}' > john.credentials @@ -191,7 +188,7 @@ To add S3 storage provider: # Project overview -In short, Datasafe [core logic](datasafe-business/src/main/java/de/adorsys/datasafe/business/impl/service/DefaultDatasafeServices.java) +In short, datasafe [core logic](datasafe-business/src/main/java/de/adorsys/datasafe/business/impl/service/DefaultDatasafeServices.java) provides these key services: * [Privatespace service](datasafe-privatestore/datasafe-privatestore-impl/src/main/java/de/adorsys/datasafe/privatestore/impl/PrivateSpaceServiceImpl.java) that securely stores private files by encrypting them using users' secret key. @@ -281,7 +278,7 @@ TODO: Migrate to AsciiDoc for automatic snippet embedding. --> ## Generic Datasafe Usage -First, you want to create Datasafe services. This snippet provides you Datasafe that uses filesystem storage adapter: +First, you want to create datasafe services. This snippet provides a datasafe that uses filesystem storage adapter: [Example:Create Datasafe services](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithDefaultDatasafeTest.java#L46-L52) ```groovy // this will create all Datasafe files and user documents under @@ -407,10 +404,10 @@ Suppose we need to preserve file history, so accidental file removal won't destr we can use storage provider that supports versioning. But if we have storage provider does not support versions (i.e. minio) we can turn-on software versioning, here is its usage examples; -First, we will obtain versioned Datasafe services that uses filesystem storage adapter: -[Example:Create versioned Datasafe services](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithVersionedDatasafeTest.java#L47-L53) +First, we will obtain versioned datasafe services that uses filesystem storage adapter: +[Example:Create versioned datasafe services](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithVersionedDatasafeTest.java#L47-L53) ```groovy -// this will create all Datasafe files and user documents under +// this will create all datasafe files and user documents under versionedServices = DaggerVersionedDatasafeServices.builder() .config(new DefaultDFSConfig(root.toAbsolutePath().toUri(), "secret"::toCharArray)) .storage(new FileSystemStorageService(root)) @@ -583,10 +580,10 @@ assertThat(defaultDatasafeServices.privateService().read( ``` ## Overriding Datasafe Functionality -Whenever you want to have some custom functionality of Datasafe, instead of default ones, there are +Whenever you want to have some custom functionality of datasafe, instead of default ones, there are two possible ways to achieve this: - using OverridesRegistry without project recompilation. -- using Dagger2 to build a customized version of Datasafe. +- using Dagger2 to build a customized version of datasafe. ### Overriding Functionality without Recompilation This approach is for classes annotated with @@ -596,7 +593,7 @@ and it works by putting the custom implementation of a class to be overridden in During runtime, when accessing desired functionality, the library will look into OverridesRegistry for custom class implementation and use it if present. This one has the advantage of not requiring recompilation of Datasafe library, but has a limitation of working on static dependency graph - you can't rebuild it. -[Example:Create overridable Datasafe services without recompilation](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/RuntimeOverrideOperationsTest.java#L31-L53) +[Example:Create overridable datasafe services without recompilation](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/RuntimeOverrideOperationsTest.java#L31-L53) ```groovy // This shows how to override path encryption service, in particular we are going to disable it OverridesRegistry registry = new BaseOverridesRegistry(); @@ -622,11 +619,11 @@ assertThat(Files.walk(root)).asString().contains("file.txt"); ``` ### Overriding Functionality by Building custom Datasafe Library -This is actually the preferred way to override something or to customize Datasafe. It has no limitations because -you can compose any Datasafe service you want using Dagger2 for dependency injection. Its major drawback is that +This is actually the preferred way to override something or to customize datasafe. It has no limitations because +you can compose any datasafe service you want using Dagger2 for dependency injection. Its major drawback is that you need to add a dependency to Dagger2 into your project and compile this custom library version. Because of compile-time dependency injection and modular structure it is a comparatively error-free approach. -To create custom Datasafe service we need to follow these 3 steps: +To create custom datasafe service we need to follow these 3 steps: 1. Create your own custom module (or modules) - see [CustomPathEncryptionModule](datasafe-examples/datasafe-examples-customize-dagger/src/main/java/de/adorsys/datasafe/examples/business/filesystem/CustomPathEncryptionModule.java) 1. Create custom Datasafe with custom module list - see [CustomlyBuiltDatasafeServices](datasafe-examples/datasafe-examples-customize-dagger/src/main/java/de/adorsys/datasafe/examples/business/filesystem/CustomlyBuiltDatasafeServices.java) 1. Use custom-built Datasafe as shown here: From faa254a6e7bd57daadb0b8e6a6b27de7dde73663 Mon Sep 17 00:00:00 2001 From: Francis Pouatcha Date: Wed, 13 Dec 2023 17:49:25 -0500 Subject: [PATCH 17/19] Consise project description Signed-off-by: Francis Pouatcha --- README.md | 802 ++------------------------------ docs/readme/Demo.md | 102 ++++ docs/readme/DeploymentModels.md | 3 + docs/readme/HowItWorks.md | 578 +++++++++++++++++++++++ 4 files changed, 732 insertions(+), 753 deletions(-) create mode 100644 docs/readme/Demo.md create mode 100644 docs/readme/DeploymentModels.md create mode 100644 docs/readme/HowItWorks.md diff --git a/README.md b/README.md index a3ab65ada..183225d4f 100644 --- a/README.md +++ b/README.md @@ -2,789 +2,85 @@ [![codecov](https://codecov.io/gh/adorsys/datasafe/branch/develop/graph/badge.svg)](https://codecov.io/gh/adorsys/datasafe) [![Maintainability](https://api.codeclimate.com/v1/badges/06ae7d4cafc3012cee85/maintainability)](https://codeclimate.com/github/adorsys/datasafe/maintainability) -# General information -Datasafe is a powerful library designed for encrypted and versioned storage of application files, providing an added layer of security to mission-critical, data sensitive applications. +# Datasafe: Secure, Encrypted and Versioned Data Storage Library -Key features of Datasafe include: +## Overview +Datasafe is a robust library tailored for developers and enterprises, offering encrypted and versioned data storage. It enhances the security of data-sensitive applications, making it ideal for mission-critical usage. -- Unique encryption keys per user, minimizing the risk of mass data breaches. -- File path encryption for complete confidentiality of user files. -- Versioned storage of application files, offering protection against ransomware attacks. -- Asynchronous per-user inboxes for secure file exchange among users. +### Key Features +- **Enhanced Security**: Each user gets unique key encryption keys, reducing the risk of widespread data breaches. +- **Full Confidentiality**: Offers full or partial file path encryption. +- **Ransomware Protection**: Versioned storage safeguards against ransomware attacks. +- **Secure File Exchange**: Features asynchronous inboxes for safe user-to-user file transfers. +- **Zero Trust Environments**: Ideal as a data encryption layer in critical data processing backends. -## Technical Information -Datasafe employs the AES-GCM algorithm for data encryption and uses CMS-envelopes ([RFC 5652](https://www.rfc-editor.org/rfc/rfc8933#RFC5652)) as an encrypted content wrapper. More details on used encryption algorithms can be found in the [security whitepaper](SECURITY.WHITEPAPER.md). +## Technical Specifications +Datasafe uses AES-GCM (and Chacha-Poly for large files) for encryption, with CMS-envelopes ([RFC 5652](https://www.rfc-editor.org/rfc/rfc8933#RFC5652)) for encrypted content wrapping. The library's flexible design allows seamless integration and customization. For more details, refer to our [Security Whitepaper](SECURITY.WHITEPAPER.md). -The library is highly configurable, leveraging Dagger2 for dependency injection and a modular architecture that allows for seamless integration into the business layer. This flexibility enables developers to customize various aspects, such as changing the encryption algorithm or disabling path encryption. Each module is designed for maximum independence and can be used separately if needed. +### Highlights +- **Configurability**: Dagger2 for dependency injection and modular design. +- **Storage Compatibility**: Tested with Amazon S3, Minio, CEPH, and local filesystems. +- **User Privacy**: Encrypts both the document and its path in each user's private space. +- **Versioning Support**: Provides application layer versioning for systems lacking native support. -Datasafe supports various storage options, including Amazon S3, Minio, and local filesystems, with the appropriate adapter. +## Getting Started -In each user's private space, both the document and its path are encrypted. A user can write a document to the recipient's inbox space using the recipient's public key, ensuring that only the intended recipient can read a document. - -For storage systems lacking native file versioning support (e.g. simple file system), datasafe provides an application layer versioning capability. - -## Technical Features -- Flexibility - you can easily change encryption and configure or customize other aspects of library -- AES encryption using CMS-envelopes for increased security and interoperability with other languages -- Secure file sharing with other users -- Extra protection layer - encryption using securely generated keys that are completely unrelated to your password -- Application side encryption - storage layer does not see plain text data -- Works with filesystem and Amazon S3 compatible storage - S3, minio, CEPH, etc. -- Encrypted file names and file paths -- Thorough application logic and performance testing - -## Deployment Model -Followings are among others possible deployment models of the datasafe application. -![Datasafe deployment model](./docs/demo/deployment-model.png) - -## Performance -Datasafe was tested for performance on the AWS. In short, an m5.xlarge AWS instance with the datasafe library can have write throughput of 50 MiB/s and a read throughput 80 MiB/s of, when using **Amazon S3 bucket** as backing storage (performance is CPU-bound and network-bound). - -Detailed performance report is here: -[Datasafe performance results](datasafe-long-run-tests/README.md) - -## Quick demo -### Datasafe-CLI -You can try datasafe as a CLI (command-line-interface) executable for encryption of your own sensitive files. -Your encrypted files can be saved either in S3 bucket or local filesystem safely, because encryption will happen -locally - on your machine (See [CLI-README](datasafe-cli/README.md) for details). - -**Download CLI executable**: - -1. [MacOS native executable](https://github.com/adorsys/datasafe/releases/download/v0.7.0/datasafe-cli-osx-x64) -1. [Linux native executable](https://github.com/adorsys/datasafe/releases/download/v0.7.0/datasafe-cli-linux-x64) -1. Windows executable (N/A yet), please use java version below -1. [Java-based jar](https://github.com/adorsys/datasafe/releases/download/v0.7.0/datasafe-cli.jar), requires JRE (1.8+), use `java -jar datasafe-cli.jar` to execute - -#### Example actions: -##### Download application and create new user: - -

New profile animation transcript - -- Download CLI application (MacOS url) - -```bash -curl -L https://github.com/adorsys/datasafe/releases/download/v0.6.0/datasafe-cli-osx-x64 > datasafe-cli && chmod +x datasafe-cli -``` -- Create file with your credentials (they also can be passed through command line) - -```bash -echo '{"username": "john", "password": "Doe", "systemPassword": "password"}' > john.credentials -``` -- Create your new user profile (credentials come from john.credentials). You can enter value or click enter to accept -the default value when prompted. - -```bash -./datasafe-cli -c john.credentials profile create -``` -
- -![new_profile](docs/demo/new_profile.gif) - -**Note**: Instead of creating file with credentials you can provide credentials directly into terminal (this is less -secure than having credentials file, but is fine for demo purposes): -```bash -./datasafe-cli -u=MeHappyUser -p=MyCoolPassword -sp=greatSystemPassword private cat secret.txt -``` -Command above will show private file `secret.txt` content for user `MeHappyUser` who has password `MyCoolPassword` and -system password `greatSystemPassword` - -##### Encrypt and decrypt some secret data for our user: - -
Encrypting/decrypting data animation transcript - -- Create some unencrypted content - -```bash -echo "Hello world" > unencrypted.txt -``` -- Encrypt and store file from above in privatespace. In privatespace it will have decrypted name `secret.txt` -```bash -./datasafe-cli -c john.credentials private cp unencrypted.txt secret.txt -``` -- Show that filename is encrypted in privatespace: - -```bash -ls private -``` - -- Show that file content is encrypted too: - -```bash -cat private/encrypted_file_name_from_above -``` - -- Decrypt file content: - -```bash -./datasafe-cli -c john.credentials private cat secret.txt -``` -
- -![encrypt_decrypt_file](docs/demo/encrypt_decrypt_file.gif) - -##### You can always list available actions in context: - -
List actions animation transcript - -- Show top-level commands - -```bash -./datasafe-cli -c john.credentials -``` - -- Show commands for privatespace - -```bash -./datasafe-cli -c john.credentials private -``` -
- -![list_actions](docs/demo/list_actions.gif) - -### REST API demo -[Here](datasafe-rest-impl/DEMO.md) you can find quick docker-based demo of project capabilities with -instructions of how to use it (REST-api based to show how to deploy as encryption server). - - -## Building the Project +### Building the Project Without tests: ```bash mvn clean install -DskipTests=true ``` -Full build: + +Full Build: ```bash mvn clean install ``` -## Adding to your Project +### Adding Datasafe to Your Project +Include Datasafe in your Maven project: -Datasafe is available from maven-central repository, you can add it to your project using: ```xml + de.adorsys datasafe-business - 0.5.0 + {datasafe.version} -``` -To add filesystem storage provider: -```xml + de.adorsys - datasafe-storage-impl-fs - 0.5.0 + datasafe-storage-impl-s3 + {datasafe.version} -``` -To add S3 storage provider: -```xml + de.adorsys - datasafe-storage-impl-s3 - 0.5.0 + datasafe-storage-impl-fs + {datasafe.version} + test ``` +## In-depth Insights +- **Performance Analysis**: Tested on AWS, Datasafe achieves up to 50 MiB/s write and 80 MiB/s read throughput with Amazon S3. [See full report](datasafe-long-run-tests/README.md). +- **Quick Demo**: Explore Datasafe's capabilities through our [quick demo](./docs/readme/Demo.md). +- **Deployment Models**: Followings are among others [possible deployment models](./docs/readme/DeploymentModels.md). +- **How It Works**: Get a look at [detailed integration information](./docs/readme/HowItWorks.md). -# Project overview -In short, datasafe [core logic](datasafe-business/src/main/java/de/adorsys/datasafe/business/impl/service/DefaultDatasafeServices.java) -provides these key services: -* [Privatespace service](datasafe-privatestore/datasafe-privatestore-impl/src/main/java/de/adorsys/datasafe/privatestore/impl/PrivateSpaceServiceImpl.java) -that securely stores private files by encrypting them using users' secret key. -* [Inbox service](datasafe-inbox/datasafe-inbox-impl/src/main/java/de/adorsys/datasafe/inbox/impl/InboxServiceImpl.java) -that allows a user to share files with someone so that the only inbox owner can read files that are -shared with him using private key. -* [User profile service](datasafe-directory/datasafe-directory-impl/src/main/java/de/adorsys/datasafe/directory/impl/profile/operations/DFSBasedProfileStorageImpl.java) -that provides user metadata, such as where is user privatespace, his keystore, etc. - -These services are automatically built from -[modules](datasafe-business/src/main/java/de/adorsys/datasafe/business/impl) -and the only thing needed from a user is to provide storage adapter - by using -[predefined](datasafe-storage) adapters, -or by implementing his own using -[this interface](datasafe-storage/datasafe-storage-api/src/main/java/de/adorsys/datasafe/storage/api/StorageService.java). - -These services have interfaces that resemble actions that you can do with file or folder on your local file system - -list,write,read,delete file or folder. So, one can think that Datasafe provides mount-points for -inbox and private space virtual folders - you get similar actions available from Datasafe service. - -Additionally, for file versioning purposes like reading only last file version, there is [versioned privatespace](datasafe-business/src/main/java/de/adorsys/datasafe/business/impl/service/VersionedDatasafeServices.java) -that supports versioned and encrypted private file storage (for storage providers that do not support versioning). - -# How it Works - -## Library Modules -![Modules map](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/modules_map.puml&fmt=svg&vvv=1&sanitize=true) - -## Users' Files - where are they? - -Whenever user wants to store or read file at some location - be it inbox or his private space, following things do happen: -1. System resolves his profile location -1. His profile is read from some storage (and typically cached, then direct cache access happens) -1. Based on his profile content, root folder where data should be read/written is deduced -1. If data is going to private space - request path is encrypted -1. Root path is prepended to request path -1. Encryption/decryption of data happens -1. Credentials required to access the storage are added ([BucketAccessService](datasafe-directory/datasafe-directory-api/src/main/java/de/adorsys/datasafe/directory/api/profile/dfs/BucketAccessService.java)) -1. Data stream with path is sent to storage adapter -1. Optionally, storage adapter analyzes based on protocol which storage service to use -1. Storage adapter stores the data - -This diagram shows path resolution flow for private space with more details. It is mostly same both for private and -inbox files, with the only difference that private files have relative path (relative to private space location) -additionally encrypted. - -![Path resolution](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/profiles/locate_profile.puml&fmt=svg&vvv=2&sanitize=true) - -## Storing Private Files - -Private files are always encrypted using users' secret symmetric key. Additionally their path is encrypted too, but -this encryption is very special in the sense that it has form of a/b/c encrypted as -encrypted(a)/encrypted(b)/encrypted(c), so that folder traversal operations are efficient. - -![How privatespace diagram](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/high-level/how_it_works_private.puml&fmt=svg&vvv=2&sanitize=true) - -| Reading files from private space | Writing files to private space | -|---|---| -| ![Read modules](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/high-level/how_it_works_private_read_modules.puml&fmt=svg&vvv=1&sanitize=true) |![Write modules](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/high-level/how_it_works_private_write_modules.puml&fmt=svg&vvv=1&sanitize=true) | - -[Details](datasafe-privatestore) - -## Sharing files with another User - -Shared files are protected using asymmetrical cryptography, so that sender encrypts file with recipient's public key -and only recipient can read it using his private key. Paths are kept unencrypted for inbox. - -![How inbox diagram](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/high-level/how_it_works_inbox.puml&fmt=svg&vvv=1&sanitize=true) - -| Reading files from inbox | Writing files to inbox | -|---|---| -| ![Read modules](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/high-level/how_it_works_inbox_read_modules.puml&fmt=svg&vvv=1&sanitize=true) |![Write modules](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/high-level/how_it_works_inbox_write_modules.puml&fmt=svg&vvv=1&sanitize=true) | - -[Details](datasafe-inbox) - -# Examples of how to Use the Library - - -## Generic Datasafe Usage -First, you want to create datasafe services. This snippet provides a datasafe that uses filesystem storage adapter: -[Example:Create Datasafe services](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithDefaultDatasafeTest.java#L46-L52) -```groovy -// this will create all Datasafe files and user documents under -defaultDatasafeServices = DaggerDefaultDatasafeServices.builder() - .config(new DefaultDFSConfig(root.toAbsolutePath().toUri(), "secret"::toCharArray)) - .storage(new FileSystemStorageService(root)) - .build(); -``` - -Second you want to add new users: -[Example:Create new user](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithDefaultDatasafeTest.java#L60-L67) -```groovy -// Creating new user with username 'user' and private/secret key password 'passwrd': -/* -IMPORTANT: For cases when user profile is stored on S3 without object locks, this requires some global -synchronization due to eventual consistency or you need to supply globally unique username on registration -*/ -defaultDatasafeServices.userProfile().registerUsingDefaults(new UserIDAuth("user", "passwrd"::toCharArray)); -``` - -After you have a user, he wants to store some data or document securely in his privatespace: -[Example:Store file in privatespace](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithDefaultDatasafeTest.java#L78-L88) -```groovy -// creating new user -UserIDAuth user = registerUser("john"); - -// writing string "Hello" to my/own/file.txt: -// note that both resulting file content and its path are encrypted: -try (OutputStream os = defaultDatasafeServices.privateService() - .write(WriteRequest.forDefaultPrivate(user, "my/own/file.txt"))) { - os.write("Hello".getBytes(StandardCharsets.UTF_8)); -} -``` - -Now user wants to read again his secured file: -[Example:Read file from privatespace](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithDefaultDatasafeTest.java#L99-L112) -```groovy -// creating new user -UserIDAuth user = registerUser("jane"); - -// writing string "Hello Jane" to my/secret.txt into users' Jane privatespace: -writeToPrivate(user, "my/secret.txt", "Hello Jane"); - -byte[] helloJane; -// reading encrypted data from my/secret.txt, note that path is also encrypted -try (InputStream is = defaultDatasafeServices.privateService() - .read(ReadRequest.forDefaultPrivate(user, "my/secret.txt"))) { - helloJane = ByteStreams.toByteArray(is); -} -``` - -But he doesn't remember the name of file he stored, so he will list all files in privatespace and read first: -[Example:Read file from privatespace using list](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithDefaultDatasafeTest.java#L246-L260) -```groovy -// creating new user -UserIDAuth user = registerUser("john"); - -// let's create 1 file: -writeToPrivate(user, "home/my/secret.txt", "secret"); - -List> johnsPrivateFilesInMy = defaultDatasafeServices.privateService() - .list(ListRequest.forDefaultPrivate(user, "home/my")).collect(Collectors.toList()); - -// we have successfully read that file -assertThat(defaultDatasafeServices.privateService().read( - ReadRequest.forPrivate(user, johnsPrivateFilesInMy.get(0).getResource().asPrivate())) -).hasContent("secret"); -``` - -Now he wants to share some data with another user: -[Example:Send file to INBOX](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithDefaultDatasafeTest.java#L124-L134) -```groovy -// create Jane, so her INBOX does exist -UserIDAuth jane = registerUser("jane"); -UserID janeUsername = new UserID("jane"); - -// We send message "Hello John" to John just by his username -try (OutputStream os = defaultDatasafeServices.inboxService() - .write(WriteRequest.forDefaultPublic(Collections.singleton(janeUsername), "hello.txt"))) { - os.write("Hello Jane".getBytes(StandardCharsets.UTF_8)); -} -``` - -Now he wants to share some data with couple of users, so that it will be encrypted once and they both -could read the file using each using own private key: -[Example:Send file to INBOX - multiple users](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithDefaultDatasafeTest.java#L146-L158) -```groovy -// create Jane, so her INBOX does exist -UserIDAuth jane = registerUser("jane"); -// create Jamie, so his INBOX does exist -UserIDAuth jamie = registerUser("jamie"); - -// We send message to both users by using their username: -try (OutputStream os = defaultDatasafeServices.inboxService().write( - WriteRequest.forDefaultPublic(ImmutableSet.of(jane.getUserID(), jamie.getUserID()), "hello.txt")) -) { - os.write("Hello Jane and Jamie".getBytes(StandardCharsets.UTF_8)); -} -``` - -And finally it is time to read data that was shared with you: -[Example:Read file from INBOX](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithDefaultDatasafeTest.java#L268-L284) -```groovy -// creating new user -UserIDAuth user = registerUser("john"); -UserID johnUsername = new UserID("john"); - -// let's create 1 file: -shareMessage(johnUsername, "home/my/shared.txt", "shared message"); - -// Lets list our INBOX -List> johnsInboxFilesInMy = defaultDatasafeServices.inboxService() - .list(ListRequest.forDefaultPrivate(user, "")).collect(Collectors.toList()); - -// we have successfully read that file -assertThat(defaultDatasafeServices.inboxService().read( - ReadRequest.forPrivate(user, johnsInboxFilesInMy.get(0).getResource().asPrivate())) -).hasContent("shared message"); -``` - -## Datasafe with file Versioning -Suppose we need to preserve file history, so accidental file removal won't destroy everything. In such case -we can use storage provider that supports versioning. But if we have storage provider does not support versions -(i.e. minio) we can turn-on software versioning, here is its usage examples; - -First, we will obtain versioned datasafe services that uses filesystem storage adapter: -[Example:Create versioned datasafe services](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithVersionedDatasafeTest.java#L47-L53) -```groovy -// this will create all datasafe files and user documents under -versionedServices = DaggerVersionedDatasafeServices.builder() - .config(new DefaultDFSConfig(root.toAbsolutePath().toUri(), "secret"::toCharArray)) - .storage(new FileSystemStorageService(root)) - .build(); -``` - -Next we will create user, this is same as in non-versioned services: -[Example:Creating user for versioned services looks same](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithVersionedDatasafeTest.java#L61-L68) -```groovy -// Creating new user: -/* -IMPORTANT: For cases when user profile is stored on S3 without object locks, this requires some global -synchronization due to eventual consistency or you need to supply globally unique username on registration -*/ -versionedServices.userProfile().registerUsingDefaults(new UserIDAuth("user", "passwrd"::toCharArray)); -``` - -This is how file versioning works when saving file multiple times: -[Example:Saving file couple of times - versioned](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithVersionedDatasafeTest.java#L80-L104) -```groovy -// creating new user -UserIDAuth user = registerUser("john"); - -// writing string "Hello " + index to my/own/file.txt 3 times: -// note that both resulting file content and its path are encrypted: -for (int i = 1; i <= 3; ++i) { - try (OutputStream os = versionedServices.latestPrivate() - .write(WriteRequest.forDefaultPrivate(user, "my/own/file.txt"))) { - os.write(("Hello " + i).getBytes(StandardCharsets.UTF_8)); - Thread.sleep(1000L); // this will change file modified dates - } -} - -// and still we read only latest file -assertThat(versionedServices.latestPrivate() - .read(ReadRequest.forDefaultPrivate(user, "my/own/file.txt")) -).hasContent("Hello 3"); -// but there are 3 versions of file stored physically in users' privatespace: -assertThat(versionedServices.privateService().list( - ListRequest.forDefaultPrivate(user, "my/own/file.txt")) -).hasSize(3); -// and still only one file visible on latest view -assertThat(versionedServices.latestPrivate().list(ListRequest.forDefaultPrivate(user, ""))).hasSize(1); -``` - -And we can work with file versions too, of course, everything is encrypted: -[Example:Lets check how to read oldest file version](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithVersionedDatasafeTest.java#L106-L122) -```groovy -// so lets collect all versions -List, PrivateResource, DFSVersion>> withVersions = - versionedServices.versionInfo().versionsOf( - ListRequest.forDefaultPrivate(user, "my/own/file.txt") - ).collect(Collectors.toList()); -// so that we can find oldest -Versioned, PrivateResource, DFSVersion> oldest = - withVersions.stream() - .sorted(Comparator.comparing(it -> it.absolute().getResource().getModifiedAt())) - .collect(Collectors.toList()) - .get(0); -// and read oldest content -assertThat(versionedServices.privateService() - .read(ReadRequest.forPrivate(user, oldest.absolute().getResource().asPrivate())) -).hasContent("Hello 1"); -``` - -Another important case to mention is how to determine if file has changed on storage compared to some copy we have: -[Example:Check if we have latest file locally](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithVersionedDatasafeTest.java#L132-L163) -```groovy -// creating new user -UserIDAuth user = registerUser("john"); - -// First lets store some file, for example John stored it from mobile phone -try (OutputStream os = versionedServices.latestPrivate() - .write(WriteRequest.forDefaultPrivate(user, "my/own/file.txt"))) { - os.write(("Hello old version").getBytes(StandardCharsets.UTF_8)); -} - -// Application on mobile phone caches file content to improve performance, so it should cache timestamp too -Instant savedOnMobile = versionedServices.latestPrivate() - .list(ListRequest.forDefaultPrivate(user, "my/own/file.txt")) - .findAny().get().getResource().getModifiedAt(); - -// Now John uses PC to write data to my/own/file.txt with some updated data -Thread.sleep(1000L); // it took some time for him to get to PC -try (OutputStream os = versionedServices.latestPrivate() - .write(WriteRequest.forDefaultPrivate(user, "my/own/file.txt"))) { - os.write(("Hello new version").getBytes(StandardCharsets.UTF_8)); -} - -// John takes his mobile phone and application checks if it needs to sync content -Instant savedOnPC = versionedServices.latestPrivate() - .list(ListRequest.forDefaultPrivate(user, "my/own/file.txt")) - .findAny().get().getResource().getModifiedAt(); - -// This indicates that we need to update our cache on mobile phone -// Modified date of saved file has changed and it is newer that our cached date -// So mobile application should download latest file version -assertThat(savedOnPC).isAfter(savedOnMobile); -``` - -## Datasafe on Versioned Storage -If you have storage for user files on **versioned S3 bucket** and want to get object version when you write -object or to read some older version encrypted object, you can follow this example of how to do that: -[Example:Versioned storage support - writing file and reading back](datasafe-examples/datasafe-examples-versioned-s3/src/test/java/de/adorsys/datasafe/examples/business/s3/BaseUserOperationsWithDefaultDatasafeOnVersionedStorageTest.java#L138-L172) -```groovy -// creating new user -UserIDAuth user = registerUser("john"); - -// writing data to my/own/file.txt 3 times with different content: -// 1st time, writing into my/own/file.txt: -// Expanded snippet of how to capture file version when writing object: -AtomicReference version = new AtomicReference<>(); -try (OutputStream os = defaultDatasafeServices.privateService() - .write(WriteRequest.forDefaultPrivate(user, MY_OWN_FILE_TXT) - .toBuilder() - .callback((PhysicalVersionCallback) version::set) - .build()) -) { - // Initial version will contain "Hello 1": - os.write("Hello 1".getBytes(StandardCharsets.UTF_8)); -} -// this variable has our initial file version: -String version1 = version.get(); - -// Write 2 more times different data to same file - my/own/file.txt: -String version2 = writeToPrivate(user, MY_OWN_FILE_TXT, "Hello 2"); -// Last version will contain "Hello 3": -String version3 = writeToPrivate(user, MY_OWN_FILE_TXT, "Hello 3"); - -// now, when we read file without specifying version - we see latest file content: -assertThat(defaultDatasafeServices.privateService().read( - ReadRequest.forDefaultPrivate(user, MY_OWN_FILE_TXT)) -).hasContent("Hello 3"); - -// but if we specify file version - we get content for it: -assertThat(defaultDatasafeServices.privateService().read( - ReadRequest.forDefaultPrivateWithVersion(user, MY_OWN_FILE_TXT, new StorageVersion(version1))) -).hasContent("Hello 1"); -``` -Removing old file version can be done by [bucket policy](https://docs.aws.amazon.com/AmazonS3/latest/dev/intro-lifecycle-rules.html#non-current-days-calculations) -or manually, using this snippet: -[Example:Versioned storage support - removing specific version](datasafe-examples/datasafe-examples-versioned-s3/src/test/java/de/adorsys/datasafe/examples/business/s3/BaseUserOperationsWithDefaultDatasafeOnVersionedStorageTest.java#L188-L215) -```groovy -// creating new user -UserIDAuth user = registerUser("john"); - -// writing data to my/own/file.txt 2 times with different content: -String versionId = writeToPrivate(user, MY_OWN_FILE_TXT, "Hello 1"); -writeToPrivate(user, MY_OWN_FILE_TXT, "Hello 2"); - -// now, we read old file version -assertThat(defaultDatasafeServices.privateService().read( - ReadRequest.forDefaultPrivateWithVersion(user, MY_OWN_FILE_TXT, new StorageVersion(versionId))) -).hasContent("Hello 1"); - -// now, we remove old file version -defaultDatasafeServices.privateService().remove( - RemoveRequest.forDefaultPrivateWithVersion(user, MY_OWN_FILE_TXT, new StorageVersion(versionId)) -); - -// it is removed from storage, so when we read it we get exception -assertThrows(AmazonS3Exception.class, () -> defaultDatasafeServices.privateService().read( - ReadRequest.forDefaultPrivateWithVersion(user, MY_OWN_FILE_TXT, new StorageVersion(versionId))) -); - -// but latest file version is still available -assertThat(defaultDatasafeServices.privateService().read( - ReadRequest.forDefaultPrivate(user, MY_OWN_FILE_TXT)) -).hasContent("Hello 2"); -``` - -## Overriding Datasafe Functionality -Whenever you want to have some custom functionality of datasafe, instead of default ones, there are -two possible ways to achieve this: -- using OverridesRegistry without project recompilation. -- using Dagger2 to build a customized version of datasafe. - -### Overriding Functionality without Recompilation -This approach is for classes annotated with -[@RuntimeDelegate](datasafe-types-api/src/main/java/de/adorsys/datasafe/types/api/context/annotations/RuntimeDelegate.java) -and it works by putting the custom implementation of a class to be overridden into -[OverridesRegistry](datasafe-types-api/src/main/java/de/adorsys/datasafe/types/api/context/overrides/OverridesRegistry.java). -During runtime, when accessing desired functionality, the library will look into OverridesRegistry for -custom class implementation and use it if present. This one has the advantage of not requiring recompilation of -Datasafe library, but has a limitation of working on static dependency graph - you can't rebuild it. -[Example:Create overridable datasafe services without recompilation](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/RuntimeOverrideOperationsTest.java#L31-L53) -```groovy -// This shows how to override path encryption service, in particular we are going to disable it -OverridesRegistry registry = new BaseOverridesRegistry(); - -// PathEncryptionImpl now will have completely different functionality -// instead of calling PathEncryptionImpl methods we will call PathEncryptionImplOverridden methods -PathEncryptionImplRuntimeDelegatable.overrideWith(registry, PathEncryptionImplOverridden::new); - -// Customized service, without creating complete module and building it: -DefaultDatasafeServices datasafeServices = DaggerDefaultDatasafeServices.builder() - .config(new DefaultDFSConfig(root.toAbsolutePath().toUri(), "secret"::toCharArray)) - .storage(new FileSystemStorageService(root)) - .overridesRegistry(registry) - .build(); - -// registering user -UserIDAuth user = new UserIDAuth("user", "passwrd"::toCharArray); -datasafeServices.userProfile().registerUsingDefaults(user); -// writing into user privatespace, note that with default implementation `file.txt` would be encrypted -datasafeServices.privateService().write(WriteRequest.forDefaultPrivate(user, "file.txt")); -// but we see raw filename here: -assertThat(Files.walk(root)).asString().contains("file.txt"); -``` - -### Overriding Functionality by Building custom Datasafe Library -This is actually the preferred way to override something or to customize datasafe. It has no limitations because -you can compose any datasafe service you want using Dagger2 for dependency injection. Its major drawback is that -you need to add a dependency to Dagger2 into your project and compile this custom library version. Because of -compile-time dependency injection and modular structure it is a comparatively error-free approach. -To create custom datasafe service we need to follow these 3 steps: -1. Create your own custom module (or modules) - see [CustomPathEncryptionModule](datasafe-examples/datasafe-examples-customize-dagger/src/main/java/de/adorsys/datasafe/examples/business/filesystem/CustomPathEncryptionModule.java) -1. Create custom Datasafe with custom module list - see [CustomlyBuiltDatasafeServices](datasafe-examples/datasafe-examples-customize-dagger/src/main/java/de/adorsys/datasafe/examples/business/filesystem/CustomlyBuiltDatasafeServices.java) -1. Use custom-built Datasafe as shown here: -[Example:Create custom-built Datasafe service](datasafe-examples/datasafe-examples-customize-dagger/src/test/java/de/adorsys/datasafe/examples/business/filesystem/CustomlyBuiltDatasafeServiceTest.java#L25-L39) -```groovy -// Customized service, we create required module using compile time DI provided by Dagger: -CustomlyBuiltDatasafeServices datasafeServices = DaggerCustomlyBuiltDatasafeServices.builder() - .config(new DefaultDFSConfig(root.toAbsolutePath().toUri(), "secret"::toCharArray)) - .storage(new FileSystemStorageService(root)) - .build(); - -// registering user -UserIDAuth user = new UserIDAuth("user", "password"::toCharArray); -datasafeServices.userProfile().registerUsingDefaults(user); -// writing into user privatespace, note that with default implementation `file.txt` would be encrypted -datasafeServices.privateService().write(WriteRequest.forDefaultPrivate(user, "file.txt")); -// but we see raw filename here: -assertThat(walk(root)).asString().contains("file.txt"); -``` - -### Customizing Datasafe to store dynamic and user-provided Credentials -In case user wants to register storage credentials himself or place keystore within credentials-protected -location one can use this example: -[Example:Datasafe with multi-dfs setup](datasafe-examples/datasafe-examples-multidfs/src/test/java/de/adorsys/datasafe/examples/business/s3/MultiDfsWithCredentialsExampleTest.java#L106-L220) -```groovy -String directoryBucketS3Uri = "s3://" + DIRECTORY_BUCKET.getBucketName() + "/"; -// static client that will be used to access `directory` bucket: -StorageService directoryStorage = new S3StorageService( - directoryClient, - DIRECTORY_BUCKET.getBucketName(), - EXECUTOR -); - -OverridesRegistry registry = new BaseOverridesRegistry(); -DefaultDatasafeServices multiDfsDatasafe = DaggerDefaultDatasafeServices - .builder() - .config(new DFSConfigWithStorageCreds(directoryBucketS3Uri, "PAZZWORT"::toCharArray)) - // This storage service will route requests to proper bucket based on URI content: - // URI with directoryBucket to `directoryStorage` - // URI with filesBucketOne will get dynamically generated S3Storage - // URI with filesBucketTwo will get dynamically generated S3Storage - .storage( - new RegexDelegatingStorage( - ImmutableMap.builder() - // bind URI that contains `directoryBucket` to directoryStorage - .put(Pattern.compile(directoryBucketS3Uri + ".+"), directoryStorage) - .put( - Pattern.compile(getDockerUri("http://127.0.0.1") + ".+"), - // Dynamically creates S3 client with bucket name equal to host value - new UriBasedAuthStorageService( - acc -> new S3StorageService( - S3ClientFactory.getClient( - acc.getEndpoint(), - acc.getRegion(), - acc.getAccessKey(), - acc.getSecretKey() - ), - // Bucket name is encoded in first path segment - acc.getBucketName(), - EXECUTOR - ) - ) - ).build() - ) - ) - .overridesRegistry(registry) - .build(); -// Instead of default BucketAccessService we will use service that reads storage access credentials from -// keystore -BucketAccessServiceImplRuntimeDelegatable.overrideWith( - registry, args -> new WithCredentialProvider(args.getStorageKeyStoreOperations()) -); - -// John will have all his private files stored on `filesBucketOne` and `filesBucketOne`. -// Depending on path of file - filesBucketOne or filesBucketTwo - requests will be routed to proper bucket. -// I.e. path filesBucketOne/path/to/file will end up in `filesBucketOne` with key path/to/file -// his profile and access credentials for `filesBucketOne` will be in `configBucket` -UserIDAuth john = new UserIDAuth("john", "secret"::toCharArray); -// Here, nothing expects John has own storage credentials: -multiDfsDatasafe.userProfile().registerUsingDefaults(john); - -// Tell system that John will use his own storage credentials - regex match: -StorageIdentifier bucketOne = new StorageIdentifier(endpointsByHost.get(FILES_BUCKET_ONE) + ".+"); -StorageIdentifier bucketTwo = new StorageIdentifier(endpointsByHost.get(FILES_BUCKET_TWO) + ".+"); -// Set location for John's credentials keystore and put storage credentials into it: -UserPrivateProfile profile = multiDfsDatasafe.userProfile().privateProfile(john); -profile.getPrivateStorage().put( - bucketOne, - new AbsoluteLocation<>(BasePrivateResource.forPrivate(endpointsByHost.get(FILES_BUCKET_ONE) + "/")) -); -profile.getPrivateStorage().put( - bucketTwo, - new AbsoluteLocation<>(BasePrivateResource.forPrivate(endpointsByHost.get(FILES_BUCKET_TWO) + "/")) -); -multiDfsDatasafe.userProfile().updatePrivateProfile(john, profile); - -// register John's DFS access for `filesBucketOne` minio bucket -multiDfsDatasafe.userProfile().registerStorageCredentials( - john, - bucketOne, - new StorageCredentials( - FILES_BUCKET_ONE.getAccessKey(), - FILES_BUCKET_ONE.getSecretKey() - ) -); -// register John's DFS access for `filesBucketTwo` minio bucket -multiDfsDatasafe.userProfile().registerStorageCredentials( - john, - bucketTwo, - new StorageCredentials( - FILES_BUCKET_TWO.getAccessKey(), - FILES_BUCKET_TWO.getSecretKey() - ) -); - -// Configuring multi-storage is done, user can use his multi-storage: - -// store this file on `filesBucketOne` -try (OutputStream os = multiDfsDatasafe.privateService() - .write(WriteRequest.forPrivate(john, bucketOne, "my/file.txt"))) { - os.write("Content on bucket number ONE".getBytes(StandardCharsets.UTF_8)); -} - -// store this file on `filesBucketTwo` -try (OutputStream os = multiDfsDatasafe.privateService() - .write(WriteRequest.forPrivate(john, bucketTwo, "my/file.txt"))) { - os.write("Content on bucket number TWO".getBytes(StandardCharsets.UTF_8)); -} - -// read file from `filesBucketOne` -assertThat(multiDfsDatasafe.privateService() - .read(ReadRequest.forPrivate(john, bucketOne, "my/file.txt")) -).hasContent("Content on bucket number ONE"); - -// read file from `filesBucketTwo` -assertThat(multiDfsDatasafe.privateService() - .read(ReadRequest.forPrivate(john, bucketTwo, "my/file.txt")) -).hasContent("Content on bucket number TWO"); -``` - -You can visit the **[project homepage](https://adorsys.github.io/datasafe)** for additional information. - -# JavaDoc -You can read JavaDoc [here](https://adorsys.github.io/datasafe/javadoc/latest/index.html) - -# Contributing -* [CodingRules](docs/codingrules/CodingRules.md) -* [Branching and commiting](docs/branching/branch-and-commit.md) -* [Deployment to maven central](docs/general/deployment_maven_central.md) - - -# FAQ on Licensing - -## What is a Dual-Licensing Model? -Under a dual-licensing model, our product is available under two licenses -- [The Affero GNU General Public License v3 (AGPL v3)](https://www.gnu.org/licenses/agpl-3.0.en.html) -- A proprietary commercial license - -If you are a developer or business that would like to review our products in detail, test and implement in your -open-source projects and share the changes back to the community, the product repository is freely available under AGPL v3. +## Additional Resources +- **Project Homepage**: Visit [adorsys.github.io/datasafe](https://adorsys.github.io/datasafe) for more information. +- **JavaDoc**: Access our detailed [JavaDoc here](https://adorsys.github.io/datasafe/javadoc/latest/index.html). -If you are a business that would like to implement our products in a commercial setting and would like to protect your -individual changes, we offer the option to license our products under a commercial license. Please contact your service provider or approach us at [sales@adorsys.com](mailto:sales@adorsys.com) +## Contributing to Datasafe +- **Contributor License**: See [Developer Certificate of Origin (DCO) Enforcement](https://github.com/adorsys/datasafe/discussions/253) +- **Coding Guidelines**: [CodingRules](docs/codingrules/CodingRules.md) +- **Branching and Committing Practices**: [Branching Guide](docs/branching/branch-and-commit.md) +- **Deployment Process**: [Maven Central Deployment](docs/general/deployment_maven_central.md) +- **Feature Requests and Comments**: [Open a Discussion Ticket](https://github.com/adorsys/datasafe/discussions) +## Dual-Licensing +Datasafe is available under two licenses: +- **Open-Source Projects**: [AGPL v3 License](https://www.gnu.org/licenses/agpl-3.0.en.html) +- **Commercial License**: Proprietary license available. Contact [sales@adorsys.com](mailto:sales@adorsys.com) for details. diff --git a/docs/readme/Demo.md b/docs/readme/Demo.md new file mode 100644 index 000000000..d183aac0f --- /dev/null +++ b/docs/readme/Demo.md @@ -0,0 +1,102 @@ +## Quick demo +### Datasafe-CLI +You can try datasafe as a CLI (command-line-interface) executable for encryption of your own sensitive files. +Your encrypted files can be saved either in S3 bucket or local filesystem safely, because encryption will happen +locally - on your machine (See [CLI-README](datasafe-cli/README.md) for details). + +**Download CLI executable**: + +1. [MacOS native executable](https://github.com/adorsys/datasafe/releases/download/v0.7.0/datasafe-cli-osx-x64) +1. [Linux native executable](https://github.com/adorsys/datasafe/releases/download/v0.7.0/datasafe-cli-linux-x64) +1. Windows executable (N/A yet), please use java version below +1. [Java-based jar](https://github.com/adorsys/datasafe/releases/download/v0.7.0/datasafe-cli.jar), requires JRE (1.8+), use `java -jar datasafe-cli.jar` to execute + +#### Example actions: +##### Download application and create new user: + +
New profile animation transcript + +- Download CLI application (MacOS url) + +```bash +curl -L https://github.com/adorsys/datasafe/releases/download/v0.6.0/datasafe-cli-osx-x64 > datasafe-cli && chmod +x datasafe-cli +``` +- Create file with your credentials (they also can be passed through command line) + +```bash +echo '{"username": "john", "password": "Doe", "systemPassword": "password"}' > john.credentials +``` +- Create your new user profile (credentials come from john.credentials). You can enter value or click enter to accept +the default value when prompted. + +```bash +./datasafe-cli -c john.credentials profile create +``` +
+ +![new_profile](docs/demo/new_profile.gif) + +**Note**: Instead of creating file with credentials you can provide credentials directly into terminal (this is less +secure than having credentials file, but is fine for demo purposes): +```bash +./datasafe-cli -u=MeHappyUser -p=MyCoolPassword -sp=greatSystemPassword private cat secret.txt +``` +Command above will show private file `secret.txt` content for user `MeHappyUser` who has password `MyCoolPassword` and +system password `greatSystemPassword` + +##### Encrypt and decrypt some secret data for our user: + +
Encrypting/decrypting data animation transcript + +- Create some unencrypted content + +```bash +echo "Hello world" > unencrypted.txt +``` +- Encrypt and store file from above in privatespace. In privatespace it will have decrypted name `secret.txt` +```bash +./datasafe-cli -c john.credentials private cp unencrypted.txt secret.txt +``` +- Show that filename is encrypted in privatespace: + +```bash +ls private +``` + +- Show that file content is encrypted too: + +```bash +cat private/encrypted_file_name_from_above +``` + +- Decrypt file content: + +```bash +./datasafe-cli -c john.credentials private cat secret.txt +``` +
+ +![encrypt_decrypt_file](docs/demo/encrypt_decrypt_file.gif) + +##### You can always list available actions in context: + +
List actions animation transcript + +- Show top-level commands + +```bash +./datasafe-cli -c john.credentials +``` + +- Show commands for privatespace + +```bash +./datasafe-cli -c john.credentials private +``` +
+ +![list_actions](docs/demo/list_actions.gif) + +### REST API demo +[Here](datasafe-rest-impl/DEMO.md) you can find quick docker-based demo of project capabilities with +instructions of how to use it (REST-api based to show how to deploy as encryption server). diff --git a/docs/readme/DeploymentModels.md b/docs/readme/DeploymentModels.md new file mode 100644 index 000000000..2106fbb7c --- /dev/null +++ b/docs/readme/DeploymentModels.md @@ -0,0 +1,3 @@ +## Deployment Models +Followings are among others possible deployment models of the datasafe application. +![Datasafe deployment model](../demo/deployment-model.png) diff --git a/docs/readme/HowItWorks.md b/docs/readme/HowItWorks.md new file mode 100644 index 000000000..20d793f1a --- /dev/null +++ b/docs/readme/HowItWorks.md @@ -0,0 +1,578 @@ +# Integrating Datasafe + +## Project overview +In short, datasafe [core logic](datasafe-business/src/main/java/de/adorsys/datasafe/business/impl/service/DefaultDatasafeServices.java) +provides these key services: +* [Privatespace service](datasafe-privatestore/datasafe-privatestore-impl/src/main/java/de/adorsys/datasafe/privatestore/impl/PrivateSpaceServiceImpl.java) +that securely stores private files by encrypting them using users' secret key. +* [Inbox service](datasafe-inbox/datasafe-inbox-impl/src/main/java/de/adorsys/datasafe/inbox/impl/InboxServiceImpl.java) +that allows a user to share files with someone so that the only inbox owner can read files that are +shared with him using private key. +* [User profile service](datasafe-directory/datasafe-directory-impl/src/main/java/de/adorsys/datasafe/directory/impl/profile/operations/DFSBasedProfileStorageImpl.java) +that provides user metadata, such as where is user privatespace, his keystore, etc. + +These services are automatically built from +[modules](datasafe-business/src/main/java/de/adorsys/datasafe/business/impl) +and the only thing needed from a user is to provide storage adapter - by using +[predefined](datasafe-storage) adapters, +or by implementing his own using +[this interface](datasafe-storage/datasafe-storage-api/src/main/java/de/adorsys/datasafe/storage/api/StorageService.java). + +These services have interfaces that resemble actions that you can do with file or folder on your local file system - +list,write,read,delete file or folder. So, one can think that Datasafe provides mount-points for +inbox and private space virtual folders - you get similar actions available from Datasafe service. + +Additionally, for file versioning purposes like reading only last file version, there is [versioned privatespace](datasafe-business/src/main/java/de/adorsys/datasafe/business/impl/service/VersionedDatasafeServices.java) +that supports versioned and encrypted private file storage (for storage providers that do not support versioning). + +# How it Works + +## Library Modules +![Modules map](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/modules_map.puml&fmt=svg&vvv=1&sanitize=true) + +## Users' Files - where are they? + +Whenever user wants to store or read file at some location - be it inbox or his private space, following things do happen: +1. System resolves his profile location +1. His profile is read from some storage (and typically cached, then direct cache access happens) +1. Based on his profile content, root folder where data should be read/written is deduced +1. If data is going to private space - request path is encrypted +1. Root path is prepended to request path +1. Encryption/decryption of data happens +1. Credentials required to access the storage are added ([BucketAccessService](datasafe-directory/datasafe-directory-api/src/main/java/de/adorsys/datasafe/directory/api/profile/dfs/BucketAccessService.java)) +1. Data stream with path is sent to storage adapter +1. Optionally, storage adapter analyzes based on protocol which storage service to use +1. Storage adapter stores the data + +This diagram shows path resolution flow for private space with more details. It is mostly same both for private and +inbox files, with the only difference that private files have relative path (relative to private space location) +additionally encrypted. + +![Path resolution](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/profiles/locate_profile.puml&fmt=svg&vvv=2&sanitize=true) + +## Storing Private Files + +Private files are always encrypted using users' secret symmetric key. Additionally their path is encrypted too, but +this encryption is very special in the sense that it has form of a/b/c encrypted as +encrypted(a)/encrypted(b)/encrypted(c), so that folder traversal operations are efficient. + +![How privatespace diagram](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/high-level/how_it_works_private.puml&fmt=svg&vvv=2&sanitize=true) + +| Reading files from private space | Writing files to private space | +|---|---| +| ![Read modules](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/high-level/how_it_works_private_read_modules.puml&fmt=svg&vvv=1&sanitize=true) |![Write modules](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/high-level/how_it_works_private_write_modules.puml&fmt=svg&vvv=1&sanitize=true) | + +[Details](datasafe-privatestore) + +## Sharing files with another User + +Shared files are protected using asymmetrical cryptography, so that sender encrypts file with recipient's public key +and only recipient can read it using his private key. Paths are kept unencrypted for inbox. + +![How inbox diagram](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/high-level/how_it_works_inbox.puml&fmt=svg&vvv=1&sanitize=true) + +| Reading files from inbox | Writing files to inbox | +|---|---| +| ![Read modules](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/high-level/how_it_works_inbox_read_modules.puml&fmt=svg&vvv=1&sanitize=true) |![Write modules](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/adorsys/datasafe/develop/docs/diagrams/high-level/how_it_works_inbox_write_modules.puml&fmt=svg&vvv=1&sanitize=true) | + +[Details](datasafe-inbox) + +# Examples of how to Use the Library + + +## Generic Datasafe Usage +First, you want to create datasafe services. This snippet provides a datasafe that uses filesystem storage adapter: +[Example:Create Datasafe services](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithDefaultDatasafeTest.java#L46-L52) +```groovy +// this will create all Datasafe files and user documents under +defaultDatasafeServices = DaggerDefaultDatasafeServices.builder() + .config(new DefaultDFSConfig(root.toAbsolutePath().toUri(), "secret"::toCharArray)) + .storage(new FileSystemStorageService(root)) + .build(); +``` + +Second you want to add new users: +[Example:Create new user](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithDefaultDatasafeTest.java#L60-L67) +```groovy +// Creating new user with username 'user' and private/secret key password 'passwrd': +/* +IMPORTANT: For cases when user profile is stored on S3 without object locks, this requires some global +synchronization due to eventual consistency or you need to supply globally unique username on registration +*/ +defaultDatasafeServices.userProfile().registerUsingDefaults(new UserIDAuth("user", "passwrd"::toCharArray)); +``` + +After you have a user, he wants to store some data or document securely in his privatespace: +[Example:Store file in privatespace](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithDefaultDatasafeTest.java#L78-L88) +```groovy +// creating new user +UserIDAuth user = registerUser("john"); + +// writing string "Hello" to my/own/file.txt: +// note that both resulting file content and its path are encrypted: +try (OutputStream os = defaultDatasafeServices.privateService() + .write(WriteRequest.forDefaultPrivate(user, "my/own/file.txt"))) { + os.write("Hello".getBytes(StandardCharsets.UTF_8)); +} +``` + +Now user wants to read again his secured file: +[Example:Read file from privatespace](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithDefaultDatasafeTest.java#L99-L112) +```groovy +// creating new user +UserIDAuth user = registerUser("jane"); + +// writing string "Hello Jane" to my/secret.txt into users' Jane privatespace: +writeToPrivate(user, "my/secret.txt", "Hello Jane"); + +byte[] helloJane; +// reading encrypted data from my/secret.txt, note that path is also encrypted +try (InputStream is = defaultDatasafeServices.privateService() + .read(ReadRequest.forDefaultPrivate(user, "my/secret.txt"))) { + helloJane = ByteStreams.toByteArray(is); +} +``` + +But he doesn't remember the name of file he stored, so he will list all files in privatespace and read first: +[Example:Read file from privatespace using list](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithDefaultDatasafeTest.java#L246-L260) +```groovy +// creating new user +UserIDAuth user = registerUser("john"); + +// let's create 1 file: +writeToPrivate(user, "home/my/secret.txt", "secret"); + +List> johnsPrivateFilesInMy = defaultDatasafeServices.privateService() + .list(ListRequest.forDefaultPrivate(user, "home/my")).collect(Collectors.toList()); + +// we have successfully read that file +assertThat(defaultDatasafeServices.privateService().read( + ReadRequest.forPrivate(user, johnsPrivateFilesInMy.get(0).getResource().asPrivate())) +).hasContent("secret"); +``` + +Now he wants to share some data with another user: +[Example:Send file to INBOX](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithDefaultDatasafeTest.java#L124-L134) +```groovy +// create Jane, so her INBOX does exist +UserIDAuth jane = registerUser("jane"); +UserID janeUsername = new UserID("jane"); + +// We send message "Hello John" to John just by his username +try (OutputStream os = defaultDatasafeServices.inboxService() + .write(WriteRequest.forDefaultPublic(Collections.singleton(janeUsername), "hello.txt"))) { + os.write("Hello Jane".getBytes(StandardCharsets.UTF_8)); +} +``` + +Now he wants to share some data with couple of users, so that it will be encrypted once and they both +could read the file using each using own private key: +[Example:Send file to INBOX - multiple users](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithDefaultDatasafeTest.java#L146-L158) +```groovy +// create Jane, so her INBOX does exist +UserIDAuth jane = registerUser("jane"); +// create Jamie, so his INBOX does exist +UserIDAuth jamie = registerUser("jamie"); + +// We send message to both users by using their username: +try (OutputStream os = defaultDatasafeServices.inboxService().write( + WriteRequest.forDefaultPublic(ImmutableSet.of(jane.getUserID(), jamie.getUserID()), "hello.txt")) +) { + os.write("Hello Jane and Jamie".getBytes(StandardCharsets.UTF_8)); +} +``` + +And finally it is time to read data that was shared with you: +[Example:Read file from INBOX](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithDefaultDatasafeTest.java#L268-L284) +```groovy +// creating new user +UserIDAuth user = registerUser("john"); +UserID johnUsername = new UserID("john"); + +// let's create 1 file: +shareMessage(johnUsername, "home/my/shared.txt", "shared message"); + +// Lets list our INBOX +List> johnsInboxFilesInMy = defaultDatasafeServices.inboxService() + .list(ListRequest.forDefaultPrivate(user, "")).collect(Collectors.toList()); + +// we have successfully read that file +assertThat(defaultDatasafeServices.inboxService().read( + ReadRequest.forPrivate(user, johnsInboxFilesInMy.get(0).getResource().asPrivate())) +).hasContent("shared message"); +``` + +## Datasafe with file Versioning +Suppose we need to preserve file history, so accidental file removal won't destroy everything. In such case +we can use storage provider that supports versioning. But if we have storage provider does not support versions +(i.e. minio) we can turn-on software versioning, here is its usage examples; + +First, we will obtain versioned datasafe services that uses filesystem storage adapter: +[Example:Create versioned datasafe services](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithVersionedDatasafeTest.java#L47-L53) +```groovy +// this will create all datasafe files and user documents under +versionedServices = DaggerVersionedDatasafeServices.builder() + .config(new DefaultDFSConfig(root.toAbsolutePath().toUri(), "secret"::toCharArray)) + .storage(new FileSystemStorageService(root)) + .build(); +``` + +Next we will create user, this is same as in non-versioned services: +[Example:Creating user for versioned services looks same](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithVersionedDatasafeTest.java#L61-L68) +```groovy +// Creating new user: +/* +IMPORTANT: For cases when user profile is stored on S3 without object locks, this requires some global +synchronization due to eventual consistency or you need to supply globally unique username on registration +*/ +versionedServices.userProfile().registerUsingDefaults(new UserIDAuth("user", "passwrd"::toCharArray)); +``` + +This is how file versioning works when saving file multiple times: +[Example:Saving file couple of times - versioned](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithVersionedDatasafeTest.java#L80-L104) +```groovy +// creating new user +UserIDAuth user = registerUser("john"); + +// writing string "Hello " + index to my/own/file.txt 3 times: +// note that both resulting file content and its path are encrypted: +for (int i = 1; i <= 3; ++i) { + try (OutputStream os = versionedServices.latestPrivate() + .write(WriteRequest.forDefaultPrivate(user, "my/own/file.txt"))) { + os.write(("Hello " + i).getBytes(StandardCharsets.UTF_8)); + Thread.sleep(1000L); // this will change file modified dates + } +} + +// and still we read only latest file +assertThat(versionedServices.latestPrivate() + .read(ReadRequest.forDefaultPrivate(user, "my/own/file.txt")) +).hasContent("Hello 3"); +// but there are 3 versions of file stored physically in users' privatespace: +assertThat(versionedServices.privateService().list( + ListRequest.forDefaultPrivate(user, "my/own/file.txt")) +).hasSize(3); +// and still only one file visible on latest view +assertThat(versionedServices.latestPrivate().list(ListRequest.forDefaultPrivate(user, ""))).hasSize(1); +``` + +And we can work with file versions too, of course, everything is encrypted: +[Example:Lets check how to read oldest file version](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithVersionedDatasafeTest.java#L106-L122) +```groovy +// so lets collect all versions +List, PrivateResource, DFSVersion>> withVersions = + versionedServices.versionInfo().versionsOf( + ListRequest.forDefaultPrivate(user, "my/own/file.txt") + ).collect(Collectors.toList()); +// so that we can find oldest +Versioned, PrivateResource, DFSVersion> oldest = + withVersions.stream() + .sorted(Comparator.comparing(it -> it.absolute().getResource().getModifiedAt())) + .collect(Collectors.toList()) + .get(0); +// and read oldest content +assertThat(versionedServices.privateService() + .read(ReadRequest.forPrivate(user, oldest.absolute().getResource().asPrivate())) +).hasContent("Hello 1"); +``` + +Another important case to mention is how to determine if file has changed on storage compared to some copy we have: +[Example:Check if we have latest file locally](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/BaseUserOperationsTestWithVersionedDatasafeTest.java#L132-L163) +```groovy +// creating new user +UserIDAuth user = registerUser("john"); + +// First lets store some file, for example John stored it from mobile phone +try (OutputStream os = versionedServices.latestPrivate() + .write(WriteRequest.forDefaultPrivate(user, "my/own/file.txt"))) { + os.write(("Hello old version").getBytes(StandardCharsets.UTF_8)); +} + +// Application on mobile phone caches file content to improve performance, so it should cache timestamp too +Instant savedOnMobile = versionedServices.latestPrivate() + .list(ListRequest.forDefaultPrivate(user, "my/own/file.txt")) + .findAny().get().getResource().getModifiedAt(); + +// Now John uses PC to write data to my/own/file.txt with some updated data +Thread.sleep(1000L); // it took some time for him to get to PC +try (OutputStream os = versionedServices.latestPrivate() + .write(WriteRequest.forDefaultPrivate(user, "my/own/file.txt"))) { + os.write(("Hello new version").getBytes(StandardCharsets.UTF_8)); +} + +// John takes his mobile phone and application checks if it needs to sync content +Instant savedOnPC = versionedServices.latestPrivate() + .list(ListRequest.forDefaultPrivate(user, "my/own/file.txt")) + .findAny().get().getResource().getModifiedAt(); + +// This indicates that we need to update our cache on mobile phone +// Modified date of saved file has changed and it is newer that our cached date +// So mobile application should download latest file version +assertThat(savedOnPC).isAfter(savedOnMobile); +``` + +## Datasafe on Versioned Storage +If you have storage for user files on **versioned S3 bucket** and want to get object version when you write +object or to read some older version encrypted object, you can follow this example of how to do that: +[Example:Versioned storage support - writing file and reading back](datasafe-examples/datasafe-examples-versioned-s3/src/test/java/de/adorsys/datasafe/examples/business/s3/BaseUserOperationsWithDefaultDatasafeOnVersionedStorageTest.java#L138-L172) +```groovy +// creating new user +UserIDAuth user = registerUser("john"); + +// writing data to my/own/file.txt 3 times with different content: +// 1st time, writing into my/own/file.txt: +// Expanded snippet of how to capture file version when writing object: +AtomicReference version = new AtomicReference<>(); +try (OutputStream os = defaultDatasafeServices.privateService() + .write(WriteRequest.forDefaultPrivate(user, MY_OWN_FILE_TXT) + .toBuilder() + .callback((PhysicalVersionCallback) version::set) + .build()) +) { + // Initial version will contain "Hello 1": + os.write("Hello 1".getBytes(StandardCharsets.UTF_8)); +} +// this variable has our initial file version: +String version1 = version.get(); + +// Write 2 more times different data to same file - my/own/file.txt: +String version2 = writeToPrivate(user, MY_OWN_FILE_TXT, "Hello 2"); +// Last version will contain "Hello 3": +String version3 = writeToPrivate(user, MY_OWN_FILE_TXT, "Hello 3"); + +// now, when we read file without specifying version - we see latest file content: +assertThat(defaultDatasafeServices.privateService().read( + ReadRequest.forDefaultPrivate(user, MY_OWN_FILE_TXT)) +).hasContent("Hello 3"); + +// but if we specify file version - we get content for it: +assertThat(defaultDatasafeServices.privateService().read( + ReadRequest.forDefaultPrivateWithVersion(user, MY_OWN_FILE_TXT, new StorageVersion(version1))) +).hasContent("Hello 1"); +``` +Removing old file version can be done by [bucket policy](https://docs.aws.amazon.com/AmazonS3/latest/dev/intro-lifecycle-rules.html#non-current-days-calculations) +or manually, using this snippet: +[Example:Versioned storage support - removing specific version](datasafe-examples/datasafe-examples-versioned-s3/src/test/java/de/adorsys/datasafe/examples/business/s3/BaseUserOperationsWithDefaultDatasafeOnVersionedStorageTest.java#L188-L215) +```groovy +// creating new user +UserIDAuth user = registerUser("john"); + +// writing data to my/own/file.txt 2 times with different content: +String versionId = writeToPrivate(user, MY_OWN_FILE_TXT, "Hello 1"); +writeToPrivate(user, MY_OWN_FILE_TXT, "Hello 2"); + +// now, we read old file version +assertThat(defaultDatasafeServices.privateService().read( + ReadRequest.forDefaultPrivateWithVersion(user, MY_OWN_FILE_TXT, new StorageVersion(versionId))) +).hasContent("Hello 1"); + +// now, we remove old file version +defaultDatasafeServices.privateService().remove( + RemoveRequest.forDefaultPrivateWithVersion(user, MY_OWN_FILE_TXT, new StorageVersion(versionId)) +); + +// it is removed from storage, so when we read it we get exception +assertThrows(AmazonS3Exception.class, () -> defaultDatasafeServices.privateService().read( + ReadRequest.forDefaultPrivateWithVersion(user, MY_OWN_FILE_TXT, new StorageVersion(versionId))) +); + +// but latest file version is still available +assertThat(defaultDatasafeServices.privateService().read( + ReadRequest.forDefaultPrivate(user, MY_OWN_FILE_TXT)) +).hasContent("Hello 2"); +``` + +## Overriding Datasafe Functionality +Whenever you want to have some custom functionality of datasafe, instead of default ones, there are +two possible ways to achieve this: +- using OverridesRegistry without project recompilation. +- using Dagger2 to build a customized version of datasafe. + +### Overriding Functionality without Recompilation +This approach is for classes annotated with +[@RuntimeDelegate](datasafe-types-api/src/main/java/de/adorsys/datasafe/types/api/context/annotations/RuntimeDelegate.java) +and it works by putting the custom implementation of a class to be overridden into +[OverridesRegistry](datasafe-types-api/src/main/java/de/adorsys/datasafe/types/api/context/overrides/OverridesRegistry.java). +During runtime, when accessing desired functionality, the library will look into OverridesRegistry for +custom class implementation and use it if present. This one has the advantage of not requiring recompilation of +Datasafe library, but has a limitation of working on static dependency graph - you can't rebuild it. +[Example:Create overridable datasafe services without recompilation](datasafe-examples/datasafe-examples-business/src/test/java/de/adorsys/datasafe/examples/business/filesystem/RuntimeOverrideOperationsTest.java#L31-L53) +```groovy +// This shows how to override path encryption service, in particular we are going to disable it +OverridesRegistry registry = new BaseOverridesRegistry(); + +// PathEncryptionImpl now will have completely different functionality +// instead of calling PathEncryptionImpl methods we will call PathEncryptionImplOverridden methods +PathEncryptionImplRuntimeDelegatable.overrideWith(registry, PathEncryptionImplOverridden::new); + +// Customized service, without creating complete module and building it: +DefaultDatasafeServices datasafeServices = DaggerDefaultDatasafeServices.builder() + .config(new DefaultDFSConfig(root.toAbsolutePath().toUri(), "secret"::toCharArray)) + .storage(new FileSystemStorageService(root)) + .overridesRegistry(registry) + .build(); + +// registering user +UserIDAuth user = new UserIDAuth("user", "passwrd"::toCharArray); +datasafeServices.userProfile().registerUsingDefaults(user); +// writing into user privatespace, note that with default implementation `file.txt` would be encrypted +datasafeServices.privateService().write(WriteRequest.forDefaultPrivate(user, "file.txt")); +// but we see raw filename here: +assertThat(Files.walk(root)).asString().contains("file.txt"); +``` + +### Overriding Functionality by Building custom Datasafe Library +This is actually the preferred way to override something or to customize datasafe. It has no limitations because +you can compose any datasafe service you want using Dagger2 for dependency injection. Its major drawback is that +you need to add a dependency to Dagger2 into your project and compile this custom library version. Because of +compile-time dependency injection and modular structure it is a comparatively error-free approach. +To create custom datasafe service we need to follow these 3 steps: +1. Create your own custom module (or modules) - see [CustomPathEncryptionModule](datasafe-examples/datasafe-examples-customize-dagger/src/main/java/de/adorsys/datasafe/examples/business/filesystem/CustomPathEncryptionModule.java) +1. Create custom Datasafe with custom module list - see [CustomlyBuiltDatasafeServices](datasafe-examples/datasafe-examples-customize-dagger/src/main/java/de/adorsys/datasafe/examples/business/filesystem/CustomlyBuiltDatasafeServices.java) +1. Use custom-built Datasafe as shown here: +[Example:Create custom-built Datasafe service](datasafe-examples/datasafe-examples-customize-dagger/src/test/java/de/adorsys/datasafe/examples/business/filesystem/CustomlyBuiltDatasafeServiceTest.java#L25-L39) +```groovy +// Customized service, we create required module using compile time DI provided by Dagger: +CustomlyBuiltDatasafeServices datasafeServices = DaggerCustomlyBuiltDatasafeServices.builder() + .config(new DefaultDFSConfig(root.toAbsolutePath().toUri(), "secret"::toCharArray)) + .storage(new FileSystemStorageService(root)) + .build(); + +// registering user +UserIDAuth user = new UserIDAuth("user", "password"::toCharArray); +datasafeServices.userProfile().registerUsingDefaults(user); +// writing into user privatespace, note that with default implementation `file.txt` would be encrypted +datasafeServices.privateService().write(WriteRequest.forDefaultPrivate(user, "file.txt")); +// but we see raw filename here: +assertThat(walk(root)).asString().contains("file.txt"); +``` + +### Customizing Datasafe to store dynamic and user-provided Credentials +In case user wants to register storage credentials himself or place keystore within credentials-protected +location one can use this example: +[Example:Datasafe with multi-dfs setup](datasafe-examples/datasafe-examples-multidfs/src/test/java/de/adorsys/datasafe/examples/business/s3/MultiDfsWithCredentialsExampleTest.java#L106-L220) +```groovy +String directoryBucketS3Uri = "s3://" + DIRECTORY_BUCKET.getBucketName() + "/"; +// static client that will be used to access `directory` bucket: +StorageService directoryStorage = new S3StorageService( + directoryClient, + DIRECTORY_BUCKET.getBucketName(), + EXECUTOR +); + +OverridesRegistry registry = new BaseOverridesRegistry(); +DefaultDatasafeServices multiDfsDatasafe = DaggerDefaultDatasafeServices + .builder() + .config(new DFSConfigWithStorageCreds(directoryBucketS3Uri, "PAZZWORT"::toCharArray)) + // This storage service will route requests to proper bucket based on URI content: + // URI with directoryBucket to `directoryStorage` + // URI with filesBucketOne will get dynamically generated S3Storage + // URI with filesBucketTwo will get dynamically generated S3Storage + .storage( + new RegexDelegatingStorage( + ImmutableMap.builder() + // bind URI that contains `directoryBucket` to directoryStorage + .put(Pattern.compile(directoryBucketS3Uri + ".+"), directoryStorage) + .put( + Pattern.compile(getDockerUri("http://127.0.0.1") + ".+"), + // Dynamically creates S3 client with bucket name equal to host value + new UriBasedAuthStorageService( + acc -> new S3StorageService( + S3ClientFactory.getClient( + acc.getEndpoint(), + acc.getRegion(), + acc.getAccessKey(), + acc.getSecretKey() + ), + // Bucket name is encoded in first path segment + acc.getBucketName(), + EXECUTOR + ) + ) + ).build() + ) + ) + .overridesRegistry(registry) + .build(); +// Instead of default BucketAccessService we will use service that reads storage access credentials from +// keystore +BucketAccessServiceImplRuntimeDelegatable.overrideWith( + registry, args -> new WithCredentialProvider(args.getStorageKeyStoreOperations()) +); + +// John will have all his private files stored on `filesBucketOne` and `filesBucketOne`. +// Depending on path of file - filesBucketOne or filesBucketTwo - requests will be routed to proper bucket. +// I.e. path filesBucketOne/path/to/file will end up in `filesBucketOne` with key path/to/file +// his profile and access credentials for `filesBucketOne` will be in `configBucket` +UserIDAuth john = new UserIDAuth("john", "secret"::toCharArray); +// Here, nothing expects John has own storage credentials: +multiDfsDatasafe.userProfile().registerUsingDefaults(john); + +// Tell system that John will use his own storage credentials - regex match: +StorageIdentifier bucketOne = new StorageIdentifier(endpointsByHost.get(FILES_BUCKET_ONE) + ".+"); +StorageIdentifier bucketTwo = new StorageIdentifier(endpointsByHost.get(FILES_BUCKET_TWO) + ".+"); +// Set location for John's credentials keystore and put storage credentials into it: +UserPrivateProfile profile = multiDfsDatasafe.userProfile().privateProfile(john); +profile.getPrivateStorage().put( + bucketOne, + new AbsoluteLocation<>(BasePrivateResource.forPrivate(endpointsByHost.get(FILES_BUCKET_ONE) + "/")) +); +profile.getPrivateStorage().put( + bucketTwo, + new AbsoluteLocation<>(BasePrivateResource.forPrivate(endpointsByHost.get(FILES_BUCKET_TWO) + "/")) +); +multiDfsDatasafe.userProfile().updatePrivateProfile(john, profile); + +// register John's DFS access for `filesBucketOne` minio bucket +multiDfsDatasafe.userProfile().registerStorageCredentials( + john, + bucketOne, + new StorageCredentials( + FILES_BUCKET_ONE.getAccessKey(), + FILES_BUCKET_ONE.getSecretKey() + ) +); +// register John's DFS access for `filesBucketTwo` minio bucket +multiDfsDatasafe.userProfile().registerStorageCredentials( + john, + bucketTwo, + new StorageCredentials( + FILES_BUCKET_TWO.getAccessKey(), + FILES_BUCKET_TWO.getSecretKey() + ) +); + +// Configuring multi-storage is done, user can use his multi-storage: + +// store this file on `filesBucketOne` +try (OutputStream os = multiDfsDatasafe.privateService() + .write(WriteRequest.forPrivate(john, bucketOne, "my/file.txt"))) { + os.write("Content on bucket number ONE".getBytes(StandardCharsets.UTF_8)); +} + +// store this file on `filesBucketTwo` +try (OutputStream os = multiDfsDatasafe.privateService() + .write(WriteRequest.forPrivate(john, bucketTwo, "my/file.txt"))) { + os.write("Content on bucket number TWO".getBytes(StandardCharsets.UTF_8)); +} + +// read file from `filesBucketOne` +assertThat(multiDfsDatasafe.privateService() + .read(ReadRequest.forPrivate(john, bucketOne, "my/file.txt")) +).hasContent("Content on bucket number ONE"); + +// read file from `filesBucketTwo` +assertThat(multiDfsDatasafe.privateService() + .read(ReadRequest.forPrivate(john, bucketTwo, "my/file.txt")) +).hasContent("Content on bucket number TWO"); +``` \ No newline at end of file From d318b9b31540fcf64e81df987c4a1b19bfcfe596 Mon Sep 17 00:00:00 2001 From: Francis Pouatcha Date: Wed, 13 Dec 2023 17:57:53 -0500 Subject: [PATCH 18/19] Fixed broken documentation links. --- README.md | 4 ++-- docs/readme/Demo.md | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 183225d4f..6a867c696 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![codecov](https://codecov.io/gh/adorsys/datasafe/branch/develop/graph/badge.svg)](https://codecov.io/gh/adorsys/datasafe) [![Maintainability](https://api.codeclimate.com/v1/badges/06ae7d4cafc3012cee85/maintainability)](https://codeclimate.com/github/adorsys/datasafe/maintainability) -# Datasafe: Secure, Encrypted and Versioned Data Storage Library +# Secure, Encrypted and Versioned Data Storage Library ## Overview Datasafe is a robust library tailored for developers and enterprises, offering encrypted and versioned data storage. It enhances the security of data-sensitive applications, making it ideal for mission-critical usage. @@ -21,7 +21,7 @@ Datasafe uses AES-GCM (and Chacha-Poly for large files) for encryption, with CMS - **Configurability**: Dagger2 for dependency injection and modular design. - **Storage Compatibility**: Tested with Amazon S3, Minio, CEPH, and local filesystems. - **User Privacy**: Encrypts both the document and its path in each user's private space. -- **Versioning Support**: Provides application layer versioning for systems lacking native support. +- **Versioning Support**: Provides application layer versioning for systems lacking native versioning support. ## Getting Started diff --git a/docs/readme/Demo.md b/docs/readme/Demo.md index d183aac0f..5ce58abc1 100644 --- a/docs/readme/Demo.md +++ b/docs/readme/Demo.md @@ -34,7 +34,7 @@ the default value when prompted. ``` -![new_profile](docs/demo/new_profile.gif) +![new_profile](../demo/new_profile.gif) **Note**: Instead of creating file with credentials you can provide credentials directly into terminal (this is less secure than having credentials file, but is fine for demo purposes): @@ -76,7 +76,7 @@ cat private/encrypted_file_name_from_above ``` -![encrypt_decrypt_file](docs/demo/encrypt_decrypt_file.gif) +![encrypt_decrypt_file](../demo/encrypt_decrypt_file.gif) ##### You can always list available actions in context: @@ -95,8 +95,8 @@ cat private/encrypted_file_name_from_above ``` -![list_actions](docs/demo/list_actions.gif) +![list_actions](../demo/list_actions.gif) ### REST API demo -[Here](datasafe-rest-impl/DEMO.md) you can find quick docker-based demo of project capabilities with +[Here](../../datasafe-rest-impl/DEMO.md) you can find quick docker-based demo of project capabilities with instructions of how to use it (REST-api based to show how to deploy as encryption server). From 046fa088768678745d88abd3bb2a963bbbba6a09 Mon Sep 17 00:00:00 2001 From: Francis Pouatcha Date: Wed, 13 Dec 2023 18:11:37 -0500 Subject: [PATCH 19/19] Reverted pom.xml modifications on this documentation branch. --- .../datasafe-storage-impl-s3/pom.xml | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/datasafe-storage/datasafe-storage-impl-s3/pom.xml b/datasafe-storage/datasafe-storage-impl-s3/pom.xml index e17dc0cb5..e1ee5a8c4 100644 --- a/datasafe-storage/datasafe-storage-impl-s3/pom.xml +++ b/datasafe-storage/datasafe-storage-impl-s3/pom.xml @@ -32,35 +32,6 @@ com.amazonaws aws-java-sdk-core - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.dataformat - jackson-dataformat-cbor - - - - - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - test - - - com.fasterxml.jackson.dataformat - jackson-dataformat-cbor - ${jackson.version} - test - - - com.fasterxml.jackson.core - jackson-annotations - ${jackson.version} - test javax.xml.bind