├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── gpg-public-key.asc ├── pom.xml └── src ├── main └── java │ └── com │ └── kosprov │ └── jargon2 │ ├── api │ ├── Jargon2.java │ └── Jargon2Exception.java │ ├── internal │ ├── ByteArrayImpl.java │ ├── HasherImpl.java │ ├── Jargon2BackendAdapter.java │ ├── SecureRandomSaltGenerator.java │ ├── VerifierImpl.java │ └── discovery │ │ ├── Jargon2BackendDiscovery.java │ │ └── Jargon2BackendDiscoveryException.java │ └── spi │ ├── Jargon2Backend.java │ └── Jargon2BackendException.java └── test ├── java └── com │ └── kosprov │ └── jargon2 │ ├── api │ ├── CapturingDummyJargon2Backend.java │ ├── DummyJargon2Backend.java │ ├── DummyProvider.java │ ├── DummySaltGenerator.java │ ├── Jargon2Test.java │ └── NonConstructableJargon2Backend.java │ └── internal │ ├── ByteArrayImplTest.java │ └── discovery │ ├── Jargon2BackendDiscoveryErrorTest.java │ └── Jargon2BackendDiscoveryTest.java └── resources └── META-INF └── services └── com.kosprov.jargon2.spi.Jargon2Backend /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | *.ipr 4 | *.iws 5 | *.log 6 | out/ 7 | target/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | dist: trusty 4 | 5 | sudo: false 6 | install: true 7 | 8 | jdk: 9 | - oraclejdk8 10 | 11 | script: if [ "${COVERITY_SCAN_BRANCH}" != 1 ]; then mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent package sonar:sonar ; fi 12 | 13 | env: 14 | global: 15 | # The next declaration is the encrypted COVERITY_SCAN_TOKEN, created 16 | # via the "travis encrypt" command using the project repo's public key 17 | - secure: "ZbfSCC0s59asvQdTUN8oG+xFwWDSiVqS8XTWvNzmIdl4747hmJuEta6Uv46n2AOXiWdJAT66yAtDv8G6TYtkUToSrEYrXyzE0GiUW30PpM2pDB/l5hg+TzVFcMfWOni2RVA6OS+rRovg2zz7sPjA9SCbrndVyes4DTpGXdQ9o4Pk1NZtdQHQj0C5aB63A+LRq1JBsVpTFkp4igfe2/T8Z/sXuCfUil18h07TFS04EBlrhap7VapjXqhLQgJgktq2SDBbfdO2/gflcfFHq2KiHcYmRRf5tAy/+gXIMbiPu/9qTppsuG2ZJ4zWmPTt5taTBkr0s1eh4xX5/hUHUwmv/lFx61iMtmY6Pq9jDTdfKL9snEB/GJ+L1RpO9TPIblOE45BDLej/Uscc8ag9zMKgdjG3Ry6XLeivcL8083NdkURCiGGVAMwELYhaSpL/0MftCiANHjSWD/G05OUZ/56E3bJWzS4WQBYeDaUmDHSVTgv1CjwW/MF8SeRzgJ44Ec30invmqsBfjdG29XGYUqmcnc2IzQDd0RKZAsZVs5tzNG/Rrdg2yZdKLjvqCvDTK7LLWch7AV6uDy8IxYxls777aNHRxEXwb0KhOr2sn10//vZPyZZmI8YKirp/Im9cBGvQoOPzdrnqK9HqmH1QukSiybzTjS06y0r8TJ6mDEU0Pgw=" 18 | 19 | before_install: 20 | - echo -n | openssl s_client -connect scan.coverity.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | sudo tee -a /etc/ssl/certs/ca- 21 | 22 | addons: 23 | sonarcloud: 24 | organization: "kosprov-github" 25 | token: 26 | secure: "df0vf+8eLfM3WzwvmFifsMej99xwZObCwLEFVcfnuozFAJW0R4RqrOK/4Axwg64p/72lwXiaAlvYgEA/qcPw5Kt4b1OI0ai13q5ooTBGn+QknWdvU5b8tvU3lT3FcHWoHexlGGKhpG266+EXE3DLnpheAunvnGYlweF3MFASD5LdRnpBoNGKh1kuFaW+cMNCcAMxigWPvwsULlkpZopteHyLFA1g+HExFn96ZoVQJeC/LY1BPhoD/B+OSq+CUQcWrnQ++gwXBDLs0nREvwkFoI9/sYkyb0hZWC0VXAp5On9Qme94b34uVDh7CqSiSC+hkrrktEe4baTCJce0jVaRTnbhGhxzhT62S9AuIMNfLRITXAoqU/4JaM1ADVzrrwxvjDjcKQt7bDXEtMnJpzDvoZWaDXUcwYWMn48ES+H8IlypGUx1P1oYUlFFiiCdrRUnBKuPcQx1JHAWjUEXLsMt9/jtgpbIsFRjJ7aReuMJyafssFJ4u2XQstVCyK7eLRZyQawvs2X84vjcvZ1U4MoxNzyPHxnNF0EQwXIiXqsa8vv2DUxGtIbC+aVm5gNMyzikYa9I+SG/dDhMnY4cMCwYYS3ebjDMS4orWf8rZHheKgO11aZ81UREd47seNjexEwQq17yj3IFoRScyjZxrybOGjfo0jYdYm9mHhOYorLqX+o=" 27 | coverity_scan: 28 | project: 29 | name: "kosprov/jargon2-api" 30 | description: "Build submitted via Travis CI" 31 | notification_email: kosprov@gmail.com 32 | build_command_prepend: "mvn clean" 33 | build_command: "mvn -DskipTests=true compile" 34 | branch_pattern: coverity_scan 35 | 36 | cache: 37 | directories: 38 | - '$HOME/.m2/repository' 39 | - '$HOME/.sonar/cache' -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## v1.1.1 4 | 5 | Release date: Jun 26, 2018 6 | 7 | - Minor fixes (backwards compatible) 8 | 9 | ## v1.1.0 10 | 11 | Release date: Jun 11, 2018 12 | 13 | - Closed issue #1 (backwards compatible) 14 | 15 | ## v1.0.2 16 | 17 | Release date: May 10, 2018 18 | 19 | - Minor improvements in code and build script (backwards compatible) 20 | 21 | ## v1.0.1 22 | 23 | Release date: Dec 30, 2017 24 | 25 | - Minor fixes on code quality (backwards compatible) 26 | 27 | ## v1.0.0 28 | 29 | Release date: Dec 29, 2017 30 | 31 | - Initial version -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Kos Prov 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jargon2: Fluent Java API for Argon2 password hashing 2 | 3 | [![Build Status](https://travis-ci.org/kosprov/jargon2-api.svg?branch=master)](https://travis-ci.org/kosprov/jargon2-api) 4 | [![Coverity Scan Build Status](https://scan.coverity.com/projects/14707/badge.svg)](https://scan.coverity.com/projects/kosprov-jargon2-api) 5 | [![Maven metadata URI](https://img.shields.io/maven-metadata/v/http/central.maven.org/maven2/com/kosprov/jargon2/jargon2-api/maven-metadata.xml.svg)](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.kosprov.jargon2%22%20AND%20a%3A%22jargon2-api%22) 6 | [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=com.kosprov.jargon2%3Ajargon2-api&metric=alert_status)](https://sonarcloud.io/dashboard/index/com.kosprov.jargon2:jargon2-api) 7 | [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=com.kosprov.jargon2%3Ajargon2-api&metric=security_rating)](https://sonarcloud.io/dashboard/index/com.kosprov.jargon2:jargon2-api) 8 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](/LICENSE) 9 | 10 | Welcome to the Jargon2 repository. 11 | 12 | This document provides all the resources to effectively use Jargon2 and utilize its capabilities. 13 | 14 | ## Introduction 15 | 16 | Jargon2 is a builder-like API to configure and use a `Hasher` and `Verifier` API to calculate and verify password hashes. 17 | 18 | Its main features are: 19 | 20 | - Support for encoded and raw hashing 21 | - Pluggable backends to allow switching implementations 22 | - Ability to set memory lanes and threads independently 23 | - Dedicated API for keyed-hashing and additional authentication data 24 | - Secure handling of sensitive data in memory 25 | - Configurable salt generation 26 | - Unified API for different input sources (`byte[]`, `char[]`, `String` etc) 27 | - Normalization of Unicode values 28 | - Fall-back low-level API for testing and cross-validation 29 | 30 | ## Security considerations 31 | 32 | This section summarizes any security considerations that come with the use of this library. Make sure you evaluate them before choosing to use Jargon2 and visit this section regularly for any updates. 33 | 34 | | Item | Description | 35 | | --- | --- | 36 | | Normalization | Choosing to convert passwords to Unicode normal form leaves short-lived copies of it in memory. See [section on normalization](#normalization) for more details. | 37 | | Backend registration by system property | If backend registration is done by system property, its value should be monitored for improper use. See [section on backends](#backends) for more details. | 38 | | Default backend native library can be bypassed | If you're using the default backend (`jargon2-native-ri-backend`), the shared library it binds to can be overridden by defining one of `-Djna.boot.library.path`, `-Djna.library.path` and `-Djna.nosys` system properties. See the [Jargon2 Backends repository](https://github.com/kosprov/jargon2-backends "Jargon2 Backends repository") for more details. | 39 | 40 | 41 | ## User's guide 42 | 43 | Jargon2 requires Java 7 (or higher) and your application would need to have two dependencies; the Jargon2 API and a backend implementation. 44 | 45 | If you're using Maven, add the following dependencies to your pom: 46 | 47 | ```xml 48 | 49 | com.kosprov.jargon2 50 | jargon2-api 51 | 1.1.1 52 | 53 | 54 | com.kosprov.jargon2 55 | jargon2-native-ri-backend 56 | 1.1.1 57 | runtime 58 | 59 | ``` 60 | 61 | The second dependency is the [default Jargon2 backend implementation](https://github.com/kosprov/jargon2-backends "Jargon2 Backends repository") that wraps the [Argon2 reference implementation](https://github.com/P-H-C/phc-winner-argon2 "Argon2 reference implementation repository") written in C. It includes x86 binaries compiled for Windows, Linux and macOS, so it should work on most systems. Backend implementations can automatically be discovered using a `java.util.ServiceLoader` so there is no build-time dependency to the backend classes. More on this on the [backends](#backends) section. 62 | 63 | > **Note**: You may need to change the version numbers to the most recent release. Keep in mind that `jargon2-api` and `jargon2-native-ri-backend` follow different release cycles and their version number would not necessarily be the same. 64 | 65 | ### Simple example 66 | 67 | The simplest possible example would be the following: 68 | 69 | ```java 70 | import static com.kosprov.jargon2.api.Jargon2.*; 71 | 72 | public class Jargon2EncodedHashExample { 73 | public static void main(String[] args) { 74 | byte[] password = "this is a password".getBytes(); 75 | 76 | // Configure the hasher 77 | Hasher hasher = jargon2Hasher() 78 | .type(Type.ARGON2d) // Data-dependent hashing 79 | .memoryCost(65536) // 64MB memory cost 80 | .timeCost(3) // 3 passes through memory 81 | .parallelism(4) // use 4 lanes and 4 threads 82 | .saltLength(16) // 16 random bytes salt 83 | .hashLength(16); // 16 bytes output hash 84 | 85 | // Set the password and calculate the encoded hash 86 | String encodedHash = hasher.password(password).encodedHash(); 87 | 88 | System.out.printf("Hash: %s%n", encodedHash); 89 | 90 | // Just get a hold on the verifier. No special configuration needed 91 | Verifier verifier = jargon2Verifier(); 92 | 93 | // Set the encoded hash, the password and verify 94 | boolean matches = verifier.hash(encodedHash).password(password).verifyEncoded(); 95 | 96 | System.out.printf("Matches: %s%n", matches); 97 | } 98 | } 99 | ``` 100 | 101 | > **Tip**: To enjoy the Jargon2 API fluency, always start with this static import: 102 | ```java 103 | import static com.kosprov.jargon2.api.Jargon2.*; 104 | ``` 105 | 106 | `Hasher` and `Verifier` are immutable (copy-on-write), thread-safe objects. Each method call returns a new copy. You usually cascade method calls to build an instance with the static configuration (type, memory cost, time cost etc) and use that prototype instance to pass all values (password, salt, ad etc) needed to calculate a specific hash. The prototype object does not change and can be reused to calculate more hashes. Since it is immutable, it can be safely accessed by multiple threads. 107 | 108 | ### Configuration options 109 | 110 | Jargon2 allows configurability on almost every piece of functionality it provides. Below, there is table with all available configuration options. 111 | 112 | **`Hasher`** 113 | 114 | | Method | Description | 115 | | --- | --- | 116 | | `backend` | Change the backend implementation for this hasher only. Multiple overloaded methods take a class name, a `Class` object or an actual backend instance (useful only for testing). | 117 | | `options` | A set of key-value pairs to be passed to the backend. Useful if the backend needs special configuration and you don't want to be limited to system properties. | 118 | | `type` | Set the Argon2 type (Argon2i, Argon2d or Argon2id). | 119 | | `version` | Set the Argon2 version (1.0 or 1.3). | 120 | | `memoryCost` | Set the number of KB of memory to fill during hash calculation. | 121 | | `timeCost` | Set the number of passes through memory before getting the final hash value. | 122 | | `parallelism` | Set the number of memory lanes and the number of threads to process lanes. Can be set independently, e.g. set lanes to 8 and threads to 4. That would calculate the hash with 8 lanes and allows to increase threads to 8 in the future. Setting more threads than lanes is allowed but are internally capped. | 123 | | `hashLength` | Set the number of bytes of the output hash. | 124 | | `saltLength` | Set the number of bytes of the automatically generated salt. This makes sense only on the encoded hash case where the salt generated internally is part of the output value. | 125 | | `saltGenerator` | Set an implementation of the `com.kosprov.jargon2.api.Jargon2.SaltGenerator` to replace the default generator that uses a singleton `java.security.SecureRandom` instance. | 126 | | `secret` | Set the key to be used for keyed hashing (HMAC). | 127 | 128 | **`Verifier`** 129 | 130 | | Method | Description | 131 | | --- | --- | 132 | | `backend` | Change the backend implementation for this verifier only. Multiple overloaded methods take a class name, a `Class` object or an actual backend instance (useful only for testing). | 133 | | `options` | A set of key-value pairs to be passed to the backend. Useful if the backend needs special configuration and you don't want to be limited to system properties. | 134 | | `type` | Set the Argon2 type (Argon2i, Argon2d or Argon2id). Used only when verifying a raw hash. Encoded hash verification reads the value from the encoded hash, itself. | 135 | | `version` | Set the Argon2 version (1.0 or 1.3). Used only when verifying a raw hash. Encoded hash verification reads the value from the encoded hash, itself. | 136 | | `memoryCost` | Set the number of KB of memory to fill during hash verification. Used only when verifying a raw hash. Encoded hash verification reads the value from the encoded hash, itself. | 137 | | `timeCost` | Set the number of passes through memory before getting the final hash value. Used only when verifying a raw hash. Encoded hash verification reads the value from the encoded hash, itself. | 138 | | `parallelism` | Set the number of memory lanes and the number of threads to process lanes. The number of lanes is used only when verifying a raw hash. Encoded hash verification reads the value from the encoded hash, itself. | 139 | | `threads` | Set the number of threads to process lanes. This makes sense to use only for encoded hash verification. The number of lanes is read from the encoded hash itself and the number of threads by this value. If left unspecified, it uses as many threads as lanes. | 140 | | `secret` | Set the key to be used for keyed hashing (HMAC). | 141 | 142 | 143 | ### Raw hashing 144 | 145 | From a configuration standpoint, raw hashing differs from encoded hashing in just two points: 146 | 147 | - The `Hasher` cannot be configured to generate the salt automatically. The salt needs to be generated (and stored) externally and passed along with the password to calculate or verify the hash. 148 | - The `Verifier` must be configured with the same options as the `Hasher`. Since there is no encoded string to derive options from, the `Verifier` needs to know what was used during hashing. 149 | 150 | Lets see a simple raw hashing example. 151 | 152 | ```java 153 | import static com.kosprov.jargon2.api.Jargon2.*; 154 | 155 | public class Jargon2RawHashExample { 156 | public static void main(String[] args) { 157 | byte[] salt = "this is a salt".getBytes(); 158 | byte[] password = "this is a password".getBytes(); 159 | 160 | Type type = Type.ARGON2d; 161 | int memoryCost = 65536; 162 | int timeCost = 3; 163 | int parallelism = 4; 164 | int hashLength = 16; 165 | 166 | // Configure the hasher 167 | Hasher hasher = jargon2Hasher() 168 | .type(type) 169 | .memoryCost(memoryCost) 170 | .timeCost(timeCost) 171 | .parallelism(parallelism) 172 | .hashLength(hashLength); 173 | 174 | // Configure the verifier with the same settings as the hasher 175 | Verifier verifier = jargon2Verifier() 176 | .type(type) 177 | .memoryCost(memoryCost) 178 | .timeCost(timeCost) 179 | .parallelism(parallelism); 180 | 181 | // Set the salt and password to calculate the raw hash 182 | byte[] rawHash = hasher.salt(salt).password(password).rawHash(); 183 | 184 | System.out.printf("Hash: %s%n", Arrays.toString(rawHash)); 185 | 186 | // Set the raw hash, salt and password and verify 187 | boolean matches = verifier.hash(rawHash).salt(salt).password(password).verifyRaw(); 188 | 189 | System.out.printf("Matches: %s%n", matches); 190 | } 191 | } 192 | ``` 193 | 194 | ### The ByteArray API 195 | 196 | Passwords and secrets are typically available as `char[]`. Converting them to `byte[]` (as this is what's needed by low-level libraries) creates a copy of the sensitive value. This copy must be safely zeroed-out after use. 197 | 198 | Jargon2 provides the `ByteArray` API to convert data to `byte[]` and wipe any copies created when not needed anymore. Currently, the data sources that can be converted to `ByteArray` are `char[]`, `String`, `java.io.InputStream` and `java.io.Reader`. 199 | 200 | ```java 201 | import static com.kosprov.jargon2.api.Jargon2.*; 202 | 203 | ... 204 | 205 | char[] password = somehowGetPassword(); 206 | 207 | ByteArray passwordByteArray = toByteArray(password); 208 | ``` 209 | 210 | Simply wrapping the `char[]` to `ByteArray` will not trigger anything. You have to code any of the following idioms: 211 | 212 | - Define it in a try-with-resources block 213 | 214 | `ByteArray` implements the `java.lang.AutoClosable` interface, so it can be used in a try-with-resources block. On block exit, `ByteArray.close()` method will wipe out any internally maintained state. 215 | ```java 216 | try (ByteArray passwordByteArray = toByteArray(password)) { 217 | // use passwordByteArray with Hasher or Verifier 218 | } 219 | // Internal byte[] copy is zeroed-out here 220 | ``` 221 | - Define it in a try-finally block 222 | 223 | If you're not very happy with `AutoClosable`'s checked exception, you can use a simple try-finally block and manually call the `ByteArray.clear()` method. 224 | ```java 225 | ByteArray passwordByteArray = toByteArray(password); 226 | try { 227 | // use passwordByteArray with Hasher or Verifier 228 | } finally { 229 | passwordByteArray.clear(); // this will zero-out the internal byte[] copy 230 | } 231 | - Make `ByteArray` instance clear memory during finalization 232 | 233 | If you're not too concerned about security, you can call `ByteArray.finalizable()` to make this instance zero out its memory during garbage collection. When (and if) the finalizer will call the `finalize()` method is beyond your control but it's better than nothing. On average, sensitive data will stay less time in memory. 234 | ```java 235 | ByteArray passwordByteArray = toByteArray(password).finalizable(); 236 | // use passwordByteArray with Hasher or Verifier and let it be garbage collected 237 | ``` 238 | 239 | #### Clearing the source 240 | 241 | Many authentication libraries like JAAS capture the user submitted password to `char[]` to allow your code to clear it when authentication is over. 242 | 243 | In the previous section we saw how `ByteArray` can clear any internally maintained state in a semi-automatic manner. When `ByteArray` wraps mutable data sources like `char[]` or `byte[]`, it can be marked to clear the data source, as well. 244 | 245 | ```java 246 | char[] password = somehowGetPassword(); 247 | try (ByteArray passwordByteArray = toByteArray(password).clearSource()) { 248 | // use passwordByteArray with Hasher or Verifier 249 | } 250 | // Internal byte[] copy AND the source char[] (password variable) are zeroed-out here 251 | ``` 252 | 253 | #### Normalization 254 | 255 | If you allow non-ASCII characters for passwords, you may encounter some rare conditions where a user is not able to authenticate due to [Unicode equivalence](https://en.wikipedia.org/wiki/Unicode_equivalence) of some non-ASCII characters. 256 | 257 | This problem is usually solved by converting the character sequence into a Unicode normal form. This needs to be done before calculating the hash and before each verification. Jargon2 `ByteArray` provides a convenience method for converting a text source into Unicode normal form before encoding it into a `byte[]`. This is simply done as: 258 | 259 | ```java 260 | char[] password = somehowGetNonAsciiPassword(); 261 | try (ByteArray passwordByteArray = toByteArray(password).normalize()) { 262 | // use passwordByteArray with Hasher or Verifier 263 | // it will be normalized to NFC before converted to bytes. 264 | } 265 | ``` 266 | > **Security consideration**. The current implementation of normalization depends on `java.text.Normalizer` which creates internal, short-lived copies of the data passed to it before returning the normalized version. This is not good for security because user passwords will stay in memory until the JVM decides to lay out some other object on top of it. On generational garbage collection schemes, this value will be created and stay on eden space and very quickly be overridden by other objects. 267 | > If you happen to know a normalizer that does not create copies (or wipes them before letting them to the garbage collector), please, open an issue. 268 | 269 | ### A more elaborate example 270 | 271 | To showcase how Jargon2 can be used effectively, we will assume a JavaEE environment (a CDI container) and we will build an application-scoped (singleton) component which will expose a hash/verify API. It will internally manage and use an HMAC key, support different numbers of lanes and threads and expose an API to test whether a hash needs to be upgraded. 272 | 273 | ```java 274 | import javax.annotation.PostConstruct; 275 | import javax.annotation.PreDestroy; 276 | import javax.enterprise.context.ApplicationScoped; 277 | import javax.inject.Inject; 278 | 279 | import static com.kosprov.jargon2.api.Jargon2.*; 280 | 281 | /** 282 | * CDI bean that exposes encoded password hashing and verification 283 | */ 284 | @ApplicationScoped // The component is thread-safe, so we can have a single object 285 | public class PasswordHasher { 286 | 287 | private Hasher hasher; // The hasher instance 288 | private Verifier verifier; // The verifier instance 289 | 290 | @Inject 291 | private Configuration config; // Assume another CDI bean that can load configuration parameters 292 | 293 | @Inject 294 | private KeyStore keyStore; // Assume another CDI bean that can load the HMAC key 295 | 296 | private ByteArray secret; // A ByteArray that will wrap the HMAC key loaded from keyStore 297 | 298 | @PostConstruct 299 | private void init() { 300 | 301 | // Load the HMAC key and wrap it to a ByteArray. 302 | // Also, mark to clear the source byte[] when we clear this instance (see destroy() below) 303 | secret = toByteArray(keyStore.loadKey()).clearSource(); 304 | 305 | // Load configuration parameters 306 | Type type = Type.valueOf(config.getString("password.hasher.argon2.type")); 307 | int memoryCost = config.getInteger("password.hasher.argon2.memoryCost"); 308 | int timeCost = config.getInteger("password.hasher.argon2.timeCost"); 309 | int lanes = config.getInteger("password.hasher.argon2.parallelism"); 310 | int saltLength = config.getInteger("password.hasher.saltLength"); 311 | int hashLength = config.getInteger("password.hasher.hashLength"); 312 | 313 | // Use N-1 cores, regardless of the number of lanes 314 | int threads = Runtime.getRuntime().availableProcessors() - 1; 315 | 316 | hasher = jargon2Hasher() 317 | // Set the HMAC key 318 | .secret(secret) 319 | // Configure hasher properties 320 | .type(type) 321 | .memoryCost(memoryCost) 322 | .timeCost(timeCost) 323 | .parallelism(lanes, threads) // set lanes and threads independently 324 | .saltLength(saltLength) 325 | .hashLength(hashLength) 326 | ; 327 | 328 | verifier = jargon2Verifier() 329 | // Set the HMAC key 330 | .secret(secret) 331 | // Configure the threads used by the verifier, regardless of the 332 | // p property found encoded in the hash 333 | .threads(threads) 334 | ; 335 | } 336 | 337 | @PreDestroy 338 | private void destroy() { 339 | // Zero out memory locations of HMAC key during undeployment 340 | secret.clear(); 341 | } 342 | 343 | /** 344 | * Calculate an Argon2 encoded hash. The password is cleared before this method returns. 345 | * 346 | * @param password The password to be hashed 347 | * @return The encoded Argon2 hash 348 | */ 349 | public String encodedHash(char[] password) { 350 | try (ByteArray passwordByteArray = toByteArray(password).clearSource()) { 351 | return hasher.password(passwordByteArray).encodedHash(); 352 | } catch (Exception e) { 353 | throw new RuntimeException("Error during password hashing", e); 354 | } 355 | } 356 | 357 | /** 358 | * Verify that the password matches with the encoded hash. The password is cleared before 359 | * this method returns. 360 | * 361 | * @param encodedHash The encoded hash 362 | * @param password The password to be verified 363 | * @return true if the password matches with the encoded hash 364 | */ 365 | public boolean verifyEncoded(String encodedHash, char[] password) { 366 | try (ByteArray passwordByteArray = toByteArray(password).clearSource()) { 367 | return verifier.hash(encodedHash).password(passwordByteArray).verifyEncoded(); 368 | } catch (Exception e) { 369 | throw new RuntimeException("Error during password verification", e); 370 | } 371 | } 372 | 373 | /** 374 | * Tests whether properties found in the encoded hash (type, version, memory cost, time cost, parallelism, 375 | * salt length and hash length) are up-to-date with respect to the current configuration. 376 | * 377 | * @param encodedHash The encoded hash to test 378 | * @return true if properties found in encoded hash match with the current configuration 379 | */ 380 | public boolean isUpdated(String encodedHash) { 381 | return hasher.propertiesMatch(encodedHash); 382 | } 383 | } 384 | ``` 385 | 386 | The rationale in setting memory lanes and processing threads independently is when you run your application on heterogeneous hardware and the number of CPU cores available on any machine varies (e.g in a cloud environment). In such a case, you could select a sensible value for memory lanes based on your best hardware, but configure the number of threads not to exceed the number of cores of the CPU on the _current_ hardware. Depending on the configuration, you could see a small but non-negligible speedup. Do your own benchmarks to decide if it's worth it. 387 | 388 | Method `isUpdated(String)` tests whether properties found in the encoded hash match with the current configuration of the hasher by delegating to `hasher.propertiesMatch(String)`. This API can help when you want to change Argon2 properties (e.g. increase memory cost to make hashes more secure) and automatically migrate current hashes, without requiring users to reset their passwords. For example, a login component could use `isUpdated(String)` like: 389 | 390 | ```java 391 | // login started 392 | // capture password from the user and load encodedHash from the database 393 | // verify encodedHash matches with password using passwordHasher component from above 394 | boolean passwordValid = passwordHasher.verifyEncoded(encodedHash, password); 395 | if (passwordValid && !passwordHasher.isUpdated(encodedHash)) { 396 | // i. The password matched. That means we have the original plaintext password 397 | // ii. The encoded hash in the database has different properties. That means Argon2 configuration 398 | // must have changed since the user created her password. We must calculate a new hash. 399 | String newHash = passwordHasher.encodedHash(password); 400 | 401 | // store newHash in the database 402 | } 403 | // continue login 404 | ``` 405 | 406 | That way, hashes will be migrated to the new configuration gradually, as users login to the application. The cost of checking the encoded hash on every successful login is extremely low, so no performance penalty is induced. 407 | 408 | ## Low-level API 409 | 410 | During development, you may not be confident you have configured `Hasher` or `Verifier` instances properly, and you need a way to cross-check the calculated hashes with the use of another API. 411 | 412 | Jargon2 provides a low-level API, very close to the backend SPI. You can use it as follows: 413 | 414 | ```java 415 | import java.security.SecureRandom; 416 | import static com.kosprov.jargon2.api.Jargon2.*; 417 | 418 | public class SimpleLowLevelExample { 419 | public static void main(String[] args) { 420 | 421 | byte[] salt = new byte[16]; 422 | SecureRandom r = new SecureRandom(); 423 | r.nextBytes(salt); 424 | 425 | byte[] password = "this is a password".getBytes(); 426 | 427 | String encodedHash = jargon2LowLevelApi() 428 | .encodedHash( 429 | Type.ARGON2d, // Data-dependent hashing 430 | Version.V13, // version 1.3 431 | 65536, // 64MB memory cost 432 | 3, // 3 passes through memory 433 | 4, // use 4 lanes and 4 threads 434 | 16, // 16 random bytes salt 435 | salt, 436 | password 437 | ); 438 | 439 | System.out.printf("Hash: %s%n", encodedHash); 440 | 441 | boolean matches = jargon2LowLevelApi() 442 | .verifyEncoded( 443 | encodedHash, 444 | password 445 | ); 446 | 447 | System.out.printf("Matches: %s%n", matches); 448 | } 449 | } 450 | ``` 451 | 452 | ## Backends 453 | 454 | Jargon2 comes with a Service Provider Interface (SPI) for backend implementations. Currently, Jargon2 offers [a backend](https://github.com/kosprov/jargon2-backends "Jargon2 Backends repository") that wraps the [Argon2 reference implementation](https://github.com/P-H-C/phc-winner-argon2 "Argon2 reference implementation repository"). In the future, there may be other more optimized implementations or a implementation written in pure Java. 455 | 456 | A Jargon2 backend is simply an implementation of the `com.kosprov.jargon2.spi.Jargon2Backend` interface. There are three ways to hook the backend into the high-level API: 457 | 458 | - Programatically 459 | 460 | Implement the backend, have it in your classpath and call one of the `backend` builder methods of `Hasher` and `Verifier` every time you want to use it. You can set the class name, the `Class` instance or the actual backend instance. This method overrides automatic discovery. 461 | 462 | - Declaratively by adding service provider metadata 463 | 464 | Jargon2 uses standard `java.util.ServiceLoader` discovery to find your implementation. Create this file: `META-INF/services/com.kosprov.jargon2.spi.Jargon2Backend` and add the class name of your backend implementation. 465 | ``` 466 | fully.qualified.name.to.BackendClass 467 | ``` 468 | It must be visible by the context classloader of the thread that initializes Jargon2. Having the backend on its own jar and packaging it alongside with `jargon2-api` will work in almost all cases. 469 | 470 | > **Caution**: For security reasons, the discovery process will fail if it finds more than one backend implementation. Make sure there's only one such file in your classpath. 471 | 472 | - Declaratively by setting a system property 473 | 474 | Start the JVM with `-Dcom.kosprov.jargon2.spi.backend=fully.qualified.name.to.BackendClass`. 475 | 476 | > **Caution**: For security reasons, you cannot combine service provider discovery and registration by system property. If you have service provider metadata in your classpath (by the method above), adding a system property will produce an error and vice versa. 477 | 478 | > **Security consideration**: Keep your security engineers alerted and have them scan or change-detect for improper use of this property. Make sure they protect it as they would protect JAAS login module or security manager system properties and configuration files. Changing the system property to a malicius implementation will leak all your user's passwords. 479 | 480 | In terms of security, the best approach would be to use service provider discovery, package the legitimate backend jar into your application (EAR, WAR, fat-JAR, etc) and sign it. Adding a malicius jar into the classpath, repackaging the application or trying to override with the system property would result in an error. 481 | 482 | If you're uncertain on which backend has been loaded, just call `toString()` on a hasher or verifier. The return value contains the backend implementation in effect. 483 | 484 | ## Performance and stability 485 | 486 | Jargon2 default backend is a wrapper of the [Argon2 reference implementation](https://github.com/P-H-C/phc-winner-argon2 "Argon2 reference implementation repository") written in C. It packages binaries that have been compiled without any CPU-specific optimizations. It does all low-level operations with standard C code where some operations can be bulked in SIMD instructions. Expect a significant performance boost just by recompiling the C code for your particular CPU type. The gains are bigger if you're hashing with large memory and time costs. The [Jargon 2 backends repository](https://github.com/kosprov/jargon2-backends "Jargon2 Backends repository") has information on how to do that. 487 | 488 | You can, also, checkout the [Jargon 2 examples repository](https://github.com/kosprov/jargon2-examples "Jargon 2 examples respository"). It contains simple stress tests and long-running stability tests and you can run the experiments on your server to find out what works best on your hardware. -------------------------------------------------------------------------------- /gpg-public-key.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | Version: GnuPG v1 3 | 4 | mQINBFiRnUMBEACn5nQhAfIQIwWdFGOG32kaPJ46TzIgUcRPLFj+l7rE4AFDaBr9 5 | 0KT6er/MHzlMWe9xztNH9EahsS6S5O+AtzfdYjsgCThcAORDFEOtYq9CBiclSdps 6 | dszuesngXFa8IkiYlzQx2POy+1m8mGGWyPvI0EWYktLCF94gfyzOPLhdWQeva4+Q 7 | AKY/tnTE8RJPiMPAgLryUwM4xkmlccm6JpeIDq9wgq/AhSmC94hzDP5AkHUCPxHN 8 | JiJWsck+O0kGnZrxgOa3JccgRLQnR/W3xwPjB+bqDWLt64beVS7WCPxkZAVKlVwL 9 | UhoSgkn3UhLv5xBuTc23B0WraRWGWRCrR8txRyP6ABu/4viDuIjQQIPqtI0YING7 10 | /5J76pMOU5/9FB8bsqb4NJcxSszQqS0ek/7Ir++ed26gexmNNuclQS37nzhRqKB7 11 | 9YMMekI12zRhgIZvpEcx5R8In9zqhb3Hi6NxK926LsabT4c7xCRqvvRJw15U/OYd 12 | 27r2a/JJVlHQHGjW4u/Vz+IwuUg8i3DOy0xKHj+VEduY2b0rUkG8VERjPhj1jypM 13 | x1E9hlIA3vPIG+WkeSYveFZeaZURmvSWBS7lrIIrQhSNxGNJ9B5aNsjeIqRLYf96 14 | j8NYv244RwdmfjGUV/OBsnfnQ4WCjf+jZsIvQyf33aXZM+5lEBEqSykNzwARAQAB 15 | tDRLb25zdGFudGlub3MgUHJvdmF0YXMgKHBlcnNvbmFsKSA8a29zcHJvdkBnbWFp 16 | bC5jb20+iQI4BBMBAgAiBQJYkZ1DAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIX 17 | gAAKCRDfnrHSQTXO0K4TD/9OSi4LlZrFOudDP2TDoOu/bhvqpsT9k2oxBPnOsAwo 18 | 9bD54V0eCza3wbkixCNY98Zx28q8GyLGFVPh0eRccZQfJpqZW3Z8cboFlHfi3xdW 19 | aR4nY9gB5CgMUFX8yKlkUCr6b6nmbTTyDjjUuNX0NMjvdfJM9VIvpA6QfibcXJGl 20 | 50udDD/3pFVf33b529z1+q+qkqZszn//9kAzT+taJUHCHnip/+oLvfyMOdhr8vEj 21 | 5SYSGjcqpUmrtdZwZT6YiyfXstwtkpgbsusKzAiC9n564j3qcQt8FjRJL1Ag1ly2 22 | a63NPkW3GuLqkRGNSLNshBDO3AQHmZerqSjMSpC4ctT3+O6b+2ZZ433G7AAxwVG9 23 | Iew1qiN3U5lnb/IcSlL8fokAwMiy5tc7OtGfETLax+9qrC/kGIeU9oS8nMU9ZyYV 24 | t3n3RRwiomEwQZeTlv7Bot0u8xGdXSGfs85QZVufuLSswVHfOnNv8hj+VlqT1koG 25 | 5mo30vZpQQfBNpC8sZAbC72fB8my3r04Puw97tNIyN9sSoi8h9CdBtp+aU8KvX1s 26 | JtqNxj38MZpueH4DYWO4doq9DzBGFhWz3s/Q3xZmxNtrWql8h56HUHoKIxcYKIFb 27 | 4X+TzLvQ0y1ocpxjIeM65kNguF0vCFjBZYuzxOGz7nZBvYu1naonIc5+M25LpNjT 28 | SrkCDQRYkZ1DARAAxZGlfMOuZqLfPHvQs2BuvDyk+WlmLqfa+kWmKWrSjENeW1ov 29 | bgkDJ83Gk150QgN6yHYfZxT6WlVtQTkDaetyCBlWPssCcDTl2z7jB+HA1VsB/zf6 30 | yWtA5Mndyk8V7tCIxWvFUYxNxeNsC+eya3hfTU5o7Rx23Gcz1X7uQcwlHWh1GsAx 31 | 7V3ek1QWqlPL9d00sNm7nd2GWDatSrXyAMuNOFrcI+d5S4/Siczdlvxp+3BPI9RD 32 | HVlHyijO5BWZKWcmjv1ne2YiDiznppo9uHQAcZo7Uv0iIuB6P0vpmZsz4PC/x7CL 33 | GJRkvm8EZPUD8l65URvqSBzWAYn1acQ/CAOkVd6c/JaP3lzDlUYQhuzKn9RWZEV8 34 | CcmcAW1v5FRAZv8HayqClDT6kdHqRxLK5jvo+HH6DFoDwyIDbQZO4KCKZr0JBnpq 35 | eDo2MKYG+R34xdwGznNF82MEZnuvamFXjzX8qccQU1yYk+JWPCXph7uPCYv2f+Dz 36 | SKWBXQIAUxXXfbwkSLO3Dy16lyKCrgeZ2KPjFYJmSzYeyB82a+CPiIPbq9wBBLBP 37 | xtbHEYAlX3/BlWDJ/mB8mjSKlW6tknFhIMi4NWNwyhNfa3iWMvJzoGUg27LHsrhx 38 | dlTp58JXm5hYqPLJT0U8vVrEyuJU/gn4LQiwJY2RkXNQn+FmIBgOxSAiYUMAEQEA 39 | AYkCHwQYAQIACQUCWJGdQwIbDAAKCRDfnrHSQTXO0KPmD/wJSk/fzwDvLtjMG9+r 40 | mq3ZTc/0yDQ9aiqF4qfFbKkATB7NsGlJbt1fQvYhIfDltcvxJQHz4O0WWPZQPe/1 41 | /LKlDJyndHemODj8oTC5CxAuNpDAQCoAUY2vUSTLNPydgKQr93CwaoSsJpApEgTu 42 | GHHtVuW1a3EdJrQWmzJoLvv9Ft0vYMHacCuXDeQkLEU8JjqBvSRQBSOr8Xsu6PcO 43 | 3KCHW0AtnLpXLGJN/MAay7j7a5TO6na7ESD0slLv5gckNvbY3Wqpk29UVwELOuhB 44 | 0Zt8lO4HfL8xE0nXU4GjpuBPguYDRh4VHd6LQC9UOyGcWjmXy204dm9gIHp1bxPB 45 | VJAjROm+k/hr71nCaQV4wVsM3f7/TU9+USPzqCAxAxUZHRdDQuzuQFiw/G3KyzAT 46 | S8+tqVVx+hE0BVD7EDtfKK0QzVIT95BC2+dFc4v/pWA6x5JPTzEc8wGL2mMWsQ2X 47 | 8WzEw/tA8DP20cP15MRhDXHALLb36p3HxTBUzw/4AMzx/cOPfQh3jo84zjTDhUah 48 | wL7l1rsJpfG/POF5zaAvdV0AvBefwP1ogQBWHwpX0MCGb/FGYXuPaB+r0njJCaFG 49 | 1WFjVEc2eR6Wc3YKmyRoLiU/CU6mW27NeCH4mJUCe41ZkA/qKJQdioILBACHj6O0 50 | 0sRbvuplnFoCZVcEAxbWN6KPxA== 51 | =6Zp2 52 | -----END PGP PUBLIC KEY BLOCK----- 53 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.kosprov.jargon2 8 | jargon2-api 9 | 1.1.1 10 | jar 11 | 12 | Jargon2 API 13 | Fluent Java API for Argon2 password hashing 14 | https://github.com/kosprov/jargon2-api 15 | 2017 16 | 17 | 18 | 19 | The Apache Software License, Version 2.0 20 | http://www.apache.org/licenses/LICENSE-2.0.txt 21 | 22 | 23 | 24 | 25 | 26 | Kos Prov 27 | kosprov@gmail.com 28 | https://github.com/kosprov 29 | 30 | owner 31 | developer 32 | 33 | 34 | 35 | 36 | 37 | scm:git:git://github.com/kosprov/jargon2-api.git 38 | scm:git:ssh://github.com:kosprov/jargon2-api.git 39 | https://github.com/kosprov/jargon2-api 40 | 41 | 42 | 43 | Travis CI 44 | https://travis-ci.org/kosprov/jargon2-api 45 | 46 | 47 | 48 | GitHub Issues 49 | https://github.com/kosprov/jargon2-api/issues 50 | 51 | 52 | 53 | 54 | ossrh 55 | https://oss.sonatype.org/content/repositories/snapshots 56 | 57 | 58 | 59 | 60 | 61 | UTF-8 62 | 1.7 63 | 1.7 64 | 4135CED0 65 | 66 | 67 | 2.21.0 68 | 3.0.1 69 | 3.0.1 70 | 1.6 71 | 1.6.8 72 | 3.2.1 73 | 1.2.0 74 | 75 | 76 | 4.12 77 | 1.3 78 | 2.0.0.0 79 | 1.11 80 | 81 | 82 | 83 | 84 | junit 85 | junit 86 | ${junit-version} 87 | test 88 | 89 | 90 | org.hamcrest 91 | hamcrest-all 92 | ${hamcrest-version} 93 | test 94 | 95 | 96 | org.hamcrest 97 | java-hamcrest 98 | ${java-hamcrest-version} 99 | test 100 | 101 | 102 | commons-codec 103 | commons-codec 104 | ${commons-codec-version} 105 | test 106 | 107 | 108 | 109 | 110 | 111 | 112 | org.apache.maven.plugins 113 | maven-surefire-plugin 114 | ${maven-surefire-plugin-version} 115 | 116 | 1 117 | false 118 | 119 | 120 | 121 | org.sonatype.plugins 122 | nexus-staging-maven-plugin 123 | ${nexus-staging-maven-plugin-version} 124 | true 125 | 126 | ossrh 127 | https://oss.sonatype.org/ 128 | true 129 | 130 | 131 | 132 | org.apache.maven.plugins 133 | maven-source-plugin 134 | ${maven-source-plugin-version} 135 | 136 | 137 | package 138 | 139 | jar-no-fork 140 | 141 | 142 | 143 | 144 | 145 | org.apache.maven.plugins 146 | maven-javadoc-plugin 147 | ${maven-javadoc-plugin-version} 148 | 149 | com.kosprov.jargon2.internal 150 | 151 | 152 | 153 | 154 | jar 155 | 156 | 157 | 158 | 159 | 160 | org.owasp 161 | dependency-check-maven 162 | ${dependency-check-maven-version} 163 | 164 | 165 | true 166 | 167 | 168 | 169 | install 170 | 171 | check 172 | 173 | 174 | 175 | 176 | 177 | org.simplify4u.plugins 178 | pgpverify-maven-plugin 179 | ${pgpverify-maven-plugin-version} 180 | 181 | 182 | verify-dependency-signatures 183 | validate 184 | 185 | check 186 | 187 | 188 | 189 | 190 | 191 | org.apache.maven.plugins 192 | maven-gpg-plugin 193 | ${maven-gpg-plugin-version} 194 | 195 | ${gpg-keyname} 196 | 197 | 198 | 199 | sign-artifacts 200 | verify 201 | 202 | sign 203 | 204 | 205 | 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /src/main/java/com/kosprov/jargon2/api/Jargon2Exception.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.api; 2 | 3 | /** 4 | * Generic exception from the Jargon2 API 5 | */ 6 | public class Jargon2Exception extends RuntimeException { 7 | 8 | public Jargon2Exception() { 9 | } 10 | 11 | public Jargon2Exception(String message) { 12 | super(message); 13 | } 14 | 15 | public Jargon2Exception(String message, Throwable cause) { 16 | super(message, cause); 17 | } 18 | 19 | public Jargon2Exception(Throwable cause) { 20 | super(cause); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/kosprov/jargon2/internal/ByteArrayImpl.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.internal; 2 | 3 | import com.kosprov.jargon2.api.Jargon2; 4 | import com.kosprov.jargon2.api.Jargon2Exception; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.Reader; 9 | import java.nio.ByteBuffer; 10 | import java.nio.CharBuffer; 11 | import java.nio.charset.*; 12 | import java.text.Normalizer; 13 | import java.util.Arrays; 14 | 15 | import static com.kosprov.jargon2.api.Jargon2.DEFAULT_NORMALIZED_FORM; 16 | import static com.kosprov.jargon2.api.Jargon2.Normalization; 17 | 18 | public class ByteArrayImpl implements Jargon2.ByteArray { 19 | 20 | private boolean cleared = false; 21 | Data data; 22 | 23 | private ByteArrayImpl(Data data) { 24 | this.data = data; 25 | } 26 | 27 | public ByteArrayImpl(InputStream value, int bufferSize) { 28 | this(new InputStreamConsumer(value, bufferSize)); 29 | } 30 | 31 | @Override 32 | public byte[] getBytes() { 33 | return data.toByteArray(); 34 | } 35 | 36 | @Override 37 | public void close() { 38 | clear(); 39 | } 40 | 41 | @Override 42 | public void clear() { 43 | if (!cleared) { 44 | data.wipe(); 45 | cleared = true; 46 | } 47 | } 48 | 49 | private class FinalizationTrigger { 50 | @Override 51 | protected void finalize() { 52 | clear(); 53 | } 54 | } 55 | 56 | private volatile FinalizationTrigger finalizationTrigger; 57 | 58 | @Override 59 | public ByteArrayImpl finalizable() { 60 | // Instantiate the finalization trigger only once for this object 61 | if (finalizationTrigger == null) { 62 | synchronized (this) { 63 | if (finalizationTrigger == null) { 64 | finalizationTrigger = new FinalizationTrigger(); 65 | } 66 | } 67 | } 68 | return this; 69 | } 70 | 71 | static byte[] encode(char[] value, Charset encoding) { 72 | CharsetEncoder encoder = encoding.newEncoder() 73 | .onMalformedInput(CodingErrorAction.REPLACE) 74 | .onUnmappableCharacter(CodingErrorAction.REPLACE); 75 | byte[] bytes = new byte[(int) encoder.maxBytesPerChar() * value.length]; 76 | encoder.reset(); 77 | ByteBuffer bytesBuffer = ByteBuffer.wrap(bytes); 78 | CharBuffer charBuffer = CharBuffer.wrap(value); 79 | try { 80 | CoderResult result = encoder.encode(charBuffer, bytesBuffer, true); 81 | if (!result.isUnderflow()) { 82 | result.throwException(); 83 | } 84 | result = encoder.flush(bytesBuffer); 85 | if (!result.isUnderflow()) { 86 | result.throwException(); 87 | } 88 | byte[] output; 89 | if (bytes.length == bytesBuffer.position()) { 90 | output = bytes; 91 | } else { 92 | output = Arrays.copyOf(bytes, bytesBuffer.position()); 93 | Arrays.fill(bytes, (byte) 0x00); 94 | } 95 | return output; 96 | } catch (CharacterCodingException e) { 97 | Arrays.fill(bytes, (byte) 0x00); 98 | throw new Jargon2Exception("Failed to encode value to " + encoding.displayName()); 99 | } 100 | } 101 | 102 | interface Data { 103 | void wipe(); 104 | byte[] toByteArray(); 105 | } 106 | 107 | interface CharSeqData extends Data { 108 | CharSeqData withEncoding(Charset encoding); 109 | CharSeqData withNormalization(Normalization normalization); 110 | } 111 | 112 | interface ExtendedData extends Data { 113 | int length(); 114 | E copyAndWipe(int length); 115 | } 116 | 117 | static class ByteArrayData implements ExtendedData { 118 | byte[] bytes; 119 | 120 | ByteArrayData(byte[] bytes) { 121 | this.bytes = bytes; 122 | } 123 | 124 | @Override 125 | public int length() { 126 | return bytes.length; 127 | } 128 | 129 | @Override 130 | public ByteArrayData copyAndWipe(int length) { 131 | try { 132 | return new ByteArrayData(Arrays.copyOf(this.bytes, length)); 133 | } finally { 134 | wipe(); 135 | } 136 | } 137 | 138 | @Override 139 | public void wipe() { 140 | Arrays.fill(this.bytes, (byte) 0x00); 141 | } 142 | 143 | @Override 144 | public byte[] toByteArray() { 145 | return bytes; 146 | } 147 | } 148 | 149 | static class CharArrayData implements ExtendedData, CharSeqData { 150 | char[] chars; 151 | byte[] bytes; 152 | Charset encoding; 153 | Normalization normalization; 154 | 155 | CharArrayData(char[] chars, Charset encoding, Normalization normalization) { 156 | this.chars = chars; 157 | this.encoding = encoding; 158 | this.normalization = normalization; 159 | } 160 | 161 | @Override 162 | public int length() { 163 | return chars.length; 164 | } 165 | 166 | @Override 167 | public CharArrayData copyAndWipe(int length) { 168 | try { 169 | return new CharArrayData(Arrays.copyOf(this.chars, length), encoding, normalization); 170 | } finally { 171 | wipe(); 172 | } 173 | } 174 | 175 | @Override 176 | public void wipe() { 177 | Arrays.fill(chars, (char) 0); 178 | if (bytes != null) { 179 | Arrays.fill(bytes, (byte) 0x00); 180 | } 181 | } 182 | 183 | @Override 184 | public byte[] toByteArray() { 185 | if (bytes == null) { 186 | char[] c = chars; 187 | if (normalization != null) { 188 | c = Normalizer.normalize(CharBuffer.wrap(chars), Normalizer.Form.valueOf(normalization.name())).toCharArray(); 189 | } 190 | bytes = encode(c, encoding); 191 | } 192 | return bytes; 193 | } 194 | 195 | @Override 196 | public CharSeqData withEncoding(Charset encoding) { 197 | return new CharArrayData(chars, encoding, normalization); 198 | } 199 | 200 | @Override 201 | public CharSeqData withNormalization(Normalization normalization) { 202 | return new CharArrayData(chars, encoding, normalization); 203 | } 204 | } 205 | 206 | abstract static class Consumer> implements Data { 207 | T stream; 208 | int bufferSize; 209 | byte[] bytes; 210 | 211 | Consumer(T stream, int bufferSize) { 212 | this.stream = stream; 213 | this.bufferSize = bufferSize; 214 | } 215 | 216 | abstract int read(E target, int offset) throws IOException; 217 | abstract E create(int length); 218 | 219 | @Override 220 | public byte[] toByteArray() { 221 | if (bytes == null) { 222 | E data = create(bufferSize); 223 | int offset = 0; 224 | int total = 0; 225 | do { 226 | E copy; 227 | try { 228 | int dataRead = read(data, offset); 229 | if (dataRead != -1) { 230 | total += dataRead; 231 | } 232 | if (dataRead < bufferSize) { 233 | if (total > 0) { 234 | copy = data.copyAndWipe(total); 235 | } else { 236 | copy = create(0); 237 | } 238 | } else { 239 | copy = data.copyAndWipe(data.length() + bufferSize); 240 | offset += bufferSize; 241 | } 242 | } catch (IOException e) { 243 | data.wipe(); 244 | throw new Jargon2Exception("Could not consume stream"); 245 | } 246 | data = copy; 247 | } while (data.length() != total); 248 | bytes = data.toByteArray(); 249 | } 250 | return bytes; 251 | } 252 | 253 | @Override 254 | public void wipe() { 255 | if (bytes != null) { 256 | Arrays.fill(bytes, (byte) 0x00); 257 | } 258 | } 259 | } 260 | 261 | static class InputStreamConsumer extends Consumer { 262 | 263 | InputStreamConsumer(InputStream stream, int bufferSize) { 264 | super(stream, bufferSize); 265 | } 266 | 267 | @Override 268 | int read(ByteArrayData target, int offset) throws IOException { 269 | return stream.read(target.bytes, offset, bufferSize); 270 | } 271 | 272 | @Override 273 | ByteArrayData create(int length) { 274 | return new ByteArrayData(new byte[length]); 275 | } 276 | } 277 | 278 | static class ReaderConsumer extends Consumer implements CharSeqData { 279 | 280 | Charset encoding; 281 | Normalization normalization; 282 | 283 | ReaderConsumer(Reader stream, int bufferSize, Charset encoding, Normalization normalization) { 284 | super(stream, bufferSize); 285 | this.encoding = encoding; 286 | this.normalization = normalization; 287 | } 288 | 289 | @Override 290 | int read(CharArrayData target, int offset) throws IOException { 291 | return stream.read(target.chars, offset, bufferSize); 292 | } 293 | 294 | @Override 295 | CharArrayData create(int length) { 296 | return new CharArrayData(new char[length], encoding, normalization); 297 | } 298 | 299 | @Override 300 | public CharSeqData withEncoding(Charset encoding) { 301 | return new ReaderConsumer(stream, bufferSize, encoding, normalization); 302 | } 303 | 304 | @Override 305 | public CharSeqData withNormalization(Normalization normalization) { 306 | return new ReaderConsumer(stream, bufferSize, encoding, normalization); 307 | } 308 | } 309 | 310 | public static class CharSeqByteArrayImpl extends ByteArrayImpl implements Jargon2.CharSeqByteArray { 311 | 312 | public CharSeqByteArrayImpl(char[] value, Charset encoding) { 313 | super(new CharArrayData(value, encoding, null)); 314 | } 315 | 316 | public CharSeqByteArrayImpl(String value, Charset encoding) { 317 | this(value.toCharArray(), encoding); 318 | } 319 | 320 | public CharSeqByteArrayImpl(Reader value, int bufferSize, Charset encoding) { 321 | super(new ReaderConsumer(value, bufferSize, encoding, null)); 322 | } 323 | 324 | @Override 325 | public CharSeqByteArrayImpl encoding(String encoding) { 326 | return encoding(Charset.forName(encoding)); 327 | } 328 | 329 | @Override 330 | public CharSeqByteArrayImpl encoding(Charset encoding) { 331 | this.data = ((CharSeqData) data).withEncoding(encoding); 332 | return this; 333 | } 334 | 335 | @Override 336 | public CharSeqByteArrayImpl normalize() { 337 | return normalize(DEFAULT_NORMALIZED_FORM); 338 | } 339 | 340 | @Override 341 | public CharSeqByteArrayImpl normalize(Normalization normalization) { 342 | this.data = ((CharSeqData) data).withNormalization(normalization); 343 | return this; 344 | } 345 | 346 | @Override 347 | public CharSeqByteArrayImpl finalizable() { 348 | return (CharSeqByteArrayImpl) super.finalizable(); 349 | } 350 | } 351 | 352 | public static class ClearableSourceByteArrayImpl extends ByteArrayImpl implements Jargon2.ClearableSourceByteArray { 353 | 354 | boolean clearSource; 355 | byte[] bytes; 356 | 357 | public ClearableSourceByteArrayImpl(byte[] value) { 358 | super(new ByteArrayData(Arrays.copyOf(value, value.length))); 359 | this.bytes = value; 360 | } 361 | 362 | @Override 363 | public ClearableSourceByteArrayImpl clearSource() { 364 | return clearSource(true); 365 | } 366 | 367 | @Override 368 | public ClearableSourceByteArrayImpl clearSource(boolean clear) { 369 | this.clearSource = clear; 370 | return this; 371 | } 372 | 373 | @Override 374 | public void clear() { 375 | if (clearSource) { 376 | Arrays.fill(bytes, (byte) 0x00); 377 | } 378 | super.clear(); 379 | } 380 | 381 | @Override 382 | public ClearableSourceByteArrayImpl finalizable() { 383 | return (ClearableSourceByteArrayImpl) super.finalizable(); 384 | } 385 | } 386 | 387 | public static class ClearableSourceCharSeqByteArrayImpl extends CharSeqByteArrayImpl implements Jargon2.ClearableSourceCharSeqByteArray { 388 | boolean clearSource; 389 | char[] chars; 390 | 391 | public ClearableSourceCharSeqByteArrayImpl(char[] value, Charset encoding) { 392 | super(Arrays.copyOf(value, value.length), encoding); 393 | this.chars = value; 394 | } 395 | 396 | @Override 397 | public ClearableSourceCharSeqByteArrayImpl clearSource() { 398 | return clearSource(true); 399 | } 400 | 401 | @Override 402 | public ClearableSourceCharSeqByteArrayImpl clearSource(boolean clear) { 403 | this.clearSource = clear; 404 | return this; 405 | } 406 | 407 | @Override 408 | public void clear() { 409 | if (clearSource) { 410 | Arrays.fill(chars, (char) 0); 411 | } 412 | super.clear(); 413 | } 414 | 415 | @Override 416 | public ClearableSourceCharSeqByteArrayImpl encoding(String encoding) { 417 | return (ClearableSourceCharSeqByteArrayImpl) super.encoding(encoding); 418 | } 419 | 420 | @Override 421 | public ClearableSourceCharSeqByteArrayImpl encoding(Charset encoding) { 422 | return (ClearableSourceCharSeqByteArrayImpl) super.encoding(encoding); 423 | } 424 | 425 | @Override 426 | public ClearableSourceCharSeqByteArrayImpl normalize() { 427 | return (ClearableSourceCharSeqByteArrayImpl) super.normalize(); 428 | } 429 | 430 | @Override 431 | public ClearableSourceCharSeqByteArrayImpl normalize(Normalization normalization) { 432 | return (ClearableSourceCharSeqByteArrayImpl) super.normalize(normalization); 433 | } 434 | 435 | @Override 436 | public ClearableSourceCharSeqByteArrayImpl finalizable() { 437 | return (ClearableSourceCharSeqByteArrayImpl) super.finalizable(); 438 | } 439 | } 440 | } -------------------------------------------------------------------------------- /src/main/java/com/kosprov/jargon2/internal/HasherImpl.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.internal; 2 | 3 | import com.kosprov.jargon2.api.Jargon2Exception; 4 | import com.kosprov.jargon2.internal.discovery.Jargon2BackendDiscovery; 5 | import com.kosprov.jargon2.spi.Jargon2Backend; 6 | 7 | import java.security.Provider; 8 | import java.text.MessageFormat; 9 | import java.util.Collections; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.regex.Pattern; 13 | 14 | import static com.kosprov.jargon2.api.Jargon2.*; 15 | 16 | public class HasherImpl implements Hasher { 17 | 18 | private Jargon2Backend backend = Jargon2BackendDiscovery.INSTANCE.getJargon2Backend(); 19 | private Map options = Collections.emptyMap(); 20 | private Type type = Type.ARGON2i; 21 | private Version version = Version.V13; 22 | private int timeCost = 3; 23 | private int memoryCost = 4096; 24 | private int lanes = 1; 25 | private int threads = 1; 26 | private int hashLength = 32; 27 | private int saltLength = 16; 28 | 29 | private byte[] salt; 30 | private byte[] password; 31 | private byte[] secret; 32 | private byte[] ad; 33 | 34 | private SaltGenerator saltGenerator = SecureRandomSaltGenerator.DEFAULT; 35 | 36 | public HasherImpl() { 37 | } 38 | 39 | private HasherImpl(HasherImpl copy) { 40 | this.backend = copy.backend; 41 | this.options = copy.options; 42 | this.type = copy.type; 43 | this.version = copy.version; 44 | this.timeCost = copy.timeCost; 45 | this.memoryCost = copy.memoryCost; 46 | this.lanes = copy.lanes; 47 | this.threads = copy.threads; 48 | this.hashLength = copy.hashLength; 49 | this.saltLength = copy.saltLength; 50 | this.salt = copy.salt; 51 | this.password = copy.password; 52 | this.secret = copy.secret; 53 | this.ad = copy.ad; 54 | this.saltGenerator = copy.saltGenerator; 55 | } 56 | 57 | @Override 58 | public HasherImpl backend(Jargon2Backend backend) { 59 | HasherImpl copy = new HasherImpl(this); 60 | copy.backend = backend; 61 | return copy; 62 | } 63 | 64 | @Override 65 | public HasherImpl backend(String backendClass) { 66 | try { 67 | return backend(Class.forName(backendClass).asSubclass(Jargon2Backend.class)); 68 | } catch (Exception e) { 69 | throw new Jargon2Exception("Could not create Jargon2Backend instance from class " + backendClass); 70 | } 71 | } 72 | 73 | @Override 74 | public HasherImpl backend(Class backendClass) { 75 | try { 76 | return backend(backendClass.newInstance()); 77 | } catch (Exception e) { 78 | throw new Jargon2Exception("Could not create Jargon2Backend instance from class " + backendClass); 79 | } 80 | } 81 | 82 | @Override 83 | public HasherImpl options(Map options) { 84 | HasherImpl copy = new HasherImpl(this); 85 | copy.options = options != null ? new HashMap<>(options) : Collections.emptyMap(); 86 | return copy; 87 | } 88 | 89 | @Override 90 | public HasherImpl type(Type type) { 91 | HasherImpl copy = new HasherImpl(this); 92 | copy.type = type; 93 | return copy; 94 | } 95 | 96 | @Override 97 | public HasherImpl version(Version version) { 98 | HasherImpl copy = new HasherImpl(this); 99 | copy.version = version; 100 | return copy; 101 | } 102 | 103 | @Override 104 | public HasherImpl timeCost(int timeCost) { 105 | HasherImpl copy = new HasherImpl(this); 106 | copy.timeCost = timeCost; 107 | return copy; 108 | } 109 | 110 | @Override 111 | public HasherImpl memoryCost(int memoryCost) { 112 | HasherImpl copy = new HasherImpl(this); 113 | copy.memoryCost = memoryCost; 114 | return copy; 115 | } 116 | 117 | @Override 118 | public HasherImpl parallelism(int parallelism) { 119 | HasherImpl copy = new HasherImpl(this); 120 | copy.lanes = parallelism; 121 | copy.threads = parallelism; 122 | return copy; 123 | } 124 | 125 | @Override 126 | public HasherImpl parallelism(int lanes, int threads) { 127 | HasherImpl copy = new HasherImpl(this); 128 | copy.lanes = lanes; 129 | copy.threads = threads; 130 | return copy; 131 | } 132 | 133 | @Override 134 | public HasherImpl hashLength(int hashLength) { 135 | HasherImpl copy = new HasherImpl(this); 136 | copy.hashLength = hashLength; 137 | return copy; 138 | } 139 | 140 | @Override 141 | public HasherImpl saltLength(int saltLength) { 142 | HasherImpl copy = new HasherImpl(this); 143 | copy.saltLength = saltLength; 144 | return copy; 145 | } 146 | 147 | @Override 148 | public HasherImpl salt(byte[] salt) { 149 | HasherImpl copy = new HasherImpl(this); 150 | copy.salt = salt; 151 | return copy; 152 | } 153 | 154 | @Override 155 | public HasherImpl salt(ByteArray salt) { 156 | HasherImpl copy = new HasherImpl(this); 157 | copy.salt = salt.getBytes(); 158 | return copy; 159 | } 160 | 161 | @Override 162 | public HasherImpl saltGenerator(SaltGenerator saltGenerator) { 163 | HasherImpl copy = new HasherImpl(this); 164 | copy.saltGenerator = saltGenerator; 165 | return copy; 166 | } 167 | 168 | @Override 169 | public HasherImpl saltGenerator(String secureRandomAlgorithm) { 170 | HasherImpl copy = new HasherImpl(this); 171 | copy.saltGenerator = new SecureRandomSaltGenerator(secureRandomAlgorithm); 172 | return copy; 173 | } 174 | 175 | @Override 176 | public HasherImpl saltGenerator(String secureRandomAlgorithm, String secureRandomProvider) { 177 | HasherImpl copy = new HasherImpl(this); 178 | copy.saltGenerator = new SecureRandomSaltGenerator(secureRandomAlgorithm, secureRandomProvider); 179 | return copy; 180 | } 181 | 182 | @Override 183 | public HasherImpl saltGenerator(String secureRandomAlgorithm, Provider secureRandomProvider) { 184 | HasherImpl copy = new HasherImpl(this); 185 | copy.saltGenerator = new SecureRandomSaltGenerator(secureRandomAlgorithm, secureRandomProvider); 186 | return copy; 187 | } 188 | 189 | @Override 190 | public HasherImpl password(byte[] password) { 191 | HasherImpl copy = new HasherImpl(this); 192 | copy.password = password; 193 | return copy; 194 | } 195 | 196 | @Override 197 | public HasherImpl password(ByteArray password) { 198 | HasherImpl copy = new HasherImpl(this); 199 | copy.password = password.getBytes(); 200 | return copy; 201 | } 202 | 203 | @Override 204 | public HasherImpl secret(byte[] secret) { 205 | HasherImpl copy = new HasherImpl(this); 206 | copy.secret = secret; 207 | return copy; 208 | } 209 | 210 | @Override 211 | public HasherImpl secret(ByteArray secret) { 212 | HasherImpl copy = new HasherImpl(this); 213 | copy.secret = secret.getBytes(); 214 | return copy; 215 | } 216 | 217 | @Override 218 | public HasherImpl ad(byte[] ad) { 219 | HasherImpl copy = new HasherImpl(this); 220 | copy.ad = ad; 221 | return copy; 222 | } 223 | 224 | @Override 225 | public HasherImpl ad(ByteArray ad) { 226 | HasherImpl copy = new HasherImpl(this); 227 | copy.ad = ad.getBytes(); 228 | return copy; 229 | } 230 | 231 | @Override 232 | public byte[] rawHash() { 233 | if (salt == null) { 234 | throw new Jargon2Exception("Missing salt for raw hashing"); 235 | } 236 | return new Jargon2BackendAdapter(backend).rawHash(type, version, memoryCost, timeCost, lanes, threads, hashLength, secret, ad, salt, password, options); 237 | } 238 | 239 | @Override 240 | public String encodedHash() { 241 | if (salt == null) { 242 | salt = new byte[saltLength]; 243 | saltGenerator.generate(salt); 244 | } 245 | return new Jargon2BackendAdapter(backend).encodedHash(type, version, memoryCost, timeCost, lanes, threads, hashLength, secret, ad, salt, password, options); 246 | } 247 | 248 | @Override 249 | public boolean propertiesMatch(String encodedHash) { 250 | Pattern pattern = encodedHashPattern(); 251 | return pattern.matcher(encodedHash).matches(); 252 | } 253 | 254 | private Pattern encodedHashPattern; 255 | 256 | private Pattern encodedHashPattern() { 257 | if (encodedHashPattern == null) { 258 | switch (version) { 259 | case V10: 260 | encodedHashPattern = 261 | Pattern.compile( 262 | MessageFormat.format( 263 | "^\\${0}\\$m={1},t={2},p={3}\\$[A-Za-z0-9+/]'{'{4}'}'\\$[A-Za-z0-9+/]'{'{5}'}'$", 264 | type.getValue(), 265 | Integer.toString(memoryCost), 266 | Integer.toString(timeCost), 267 | Integer.toString(lanes), 268 | Integer.toString(base64Length(saltLength)), 269 | Integer.toString(base64Length(hashLength)) 270 | ) 271 | ); 272 | break; 273 | case V13: 274 | default: 275 | encodedHashPattern = 276 | Pattern.compile( 277 | MessageFormat.format( 278 | "^\\${0}\\$v={1}\\$m={2},t={3},p={4}\\$[A-Za-z0-9+/]'{'{5}'}'\\$[A-Za-z0-9+/]'{'{6}'}'$", 279 | type.getValue(), 280 | Integer.toString(version.getValue()), 281 | Integer.toString(memoryCost), 282 | Integer.toString(timeCost), 283 | Integer.toString(lanes), 284 | Integer.toString(base64Length(saltLength)), 285 | Integer.toString(base64Length(hashLength)) 286 | ) 287 | ); 288 | } 289 | } 290 | return encodedHashPattern; 291 | } 292 | 293 | private static int base64Length(int bytes) { 294 | int base64Length = bytes / 3 * 4; 295 | int mod3 = bytes % 3; 296 | if (mod3 != 0) { 297 | base64Length += (mod3 + 1); 298 | } 299 | return base64Length; 300 | } 301 | 302 | @Override 303 | public String toString() { 304 | // Careful not to leak any sensitive data 305 | return "Hasher{" + 306 | "backend=" + backend.getClass().getName() + 307 | ", options=" + options.size() + " item(s)" + 308 | ", type=" + type + 309 | ", version=" + version + 310 | ", timeCost=" + timeCost + 311 | ", memoryCost=" + memoryCost + 312 | ", lanes=" + lanes + 313 | ", threads=" + threads + 314 | ", hashLength=" + hashLength + 315 | ", saltLength=" + (salt != null ? salt.length : saltLength) + 316 | '}'; 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/main/java/com/kosprov/jargon2/internal/Jargon2BackendAdapter.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.internal; 2 | 3 | import com.kosprov.jargon2.spi.Jargon2Backend; 4 | 5 | import java.util.Map; 6 | 7 | import static com.kosprov.jargon2.api.Jargon2.*; 8 | 9 | public class Jargon2BackendAdapter implements LowLevelApi { 10 | private Jargon2Backend backend; 11 | 12 | public Jargon2BackendAdapter(Jargon2Backend backend) { 13 | this.backend = backend; 14 | } 15 | 16 | @Override 17 | public byte[] rawHash(Type type, Version version, int memoryCost, int timeCost, int parallelism, int hashLength, byte[] salt, byte[] password) { 18 | return backend.rawHash(type, version, memoryCost, timeCost, parallelism, parallelism, hashLength, null, null, salt, password, null); 19 | } 20 | 21 | @Override 22 | public byte[] rawHash(Type type, Version version, int memoryCost, int timeCost, int lanes, int threads, int hashLength, byte[] secret, byte[] ad, byte[] salt, byte[] password, Map options) { 23 | return backend.rawHash(type, version, memoryCost, timeCost, lanes, threads, hashLength, secret, ad, salt, password, options); 24 | } 25 | 26 | @Override 27 | public String encodedHash(Type type, Version version, int memoryCost, int timeCost, int parallelism, int hashLength, byte[] salt, byte[] password) { 28 | return backend.encodedHash(type, version, memoryCost, timeCost, parallelism, parallelism, hashLength, null, null, salt, password, null); 29 | } 30 | 31 | @Override 32 | public String encodedHash(Type type, Version version, int memoryCost, int timeCost, int lanes, int threads, int hashLength, byte[] secret, byte[] ad, byte[] salt, byte[] password, Map options) { 33 | return backend.encodedHash(type, version, memoryCost, timeCost, lanes, threads, hashLength, secret, ad, salt, password, options); 34 | } 35 | 36 | @Override 37 | public boolean verifyRaw(Type type, Version version, int memoryCost, int timeCost, int parallelism, byte[] rawHash, byte[] salt, byte[] password) { 38 | return backend.verifyRaw(type, version, memoryCost, timeCost, parallelism, parallelism, rawHash, null, null, salt, password, null); 39 | } 40 | 41 | @Override 42 | public boolean verifyRaw(Type type, Version version, int memoryCost, int timeCost, int lanes, int threads, byte[] rawHash, byte[] secret, byte[] ad, byte[] salt, byte[] password, Map options) { 43 | return backend.verifyRaw(type, version, memoryCost, timeCost, lanes, threads, rawHash, secret, ad, salt, password, options); 44 | } 45 | 46 | @Override 47 | public boolean verifyEncoded(String encodedHash, byte[] password) { 48 | return backend.verifyEncoded(encodedHash, -1, null, null, password, null); 49 | } 50 | 51 | @Override 52 | public boolean verifyEncoded(String encodedHash, byte[] secret, byte[] ad, byte[] password, Map options) { 53 | return backend.verifyEncoded(encodedHash, -1, secret, ad, password, options); 54 | } 55 | 56 | @Override 57 | public boolean verifyEncoded(String encodedHash, int threads, byte[] password) { 58 | return backend.verifyEncoded(encodedHash, threads, null, null, password, null); 59 | } 60 | 61 | @Override 62 | public boolean verifyEncoded(String encodedHash, int threads, byte[] secret, byte[] ad, byte[] password, Map options) { 63 | return backend.verifyEncoded(encodedHash, threads, secret, ad, password, options); 64 | } 65 | } -------------------------------------------------------------------------------- /src/main/java/com/kosprov/jargon2/internal/SecureRandomSaltGenerator.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.internal; 2 | 3 | import com.kosprov.jargon2.api.Jargon2; 4 | import com.kosprov.jargon2.api.Jargon2Exception; 5 | 6 | import java.security.NoSuchAlgorithmException; 7 | import java.security.NoSuchProviderException; 8 | import java.security.Provider; 9 | import java.security.SecureRandom; 10 | 11 | class SecureRandomSaltGenerator implements Jargon2.SaltGenerator { 12 | 13 | private static final String INSTANTIATION_FAILURE = "Failed to instantiate internal salt generator"; 14 | 15 | static final Jargon2.SaltGenerator DEFAULT = new SecureRandomSaltGenerator(); 16 | 17 | final SecureRandom random; 18 | 19 | SecureRandomSaltGenerator() { 20 | random = createSecureRandom(); 21 | } 22 | 23 | SecureRandomSaltGenerator(String algorithm) { 24 | random = createSecureRandom(algorithm); 25 | } 26 | 27 | SecureRandomSaltGenerator(String algorithm, Provider provider) { 28 | random = createSecureRandom(algorithm, provider); 29 | } 30 | 31 | SecureRandomSaltGenerator(String algorithm, String provider) { 32 | random = createSecureRandom(algorithm, provider); 33 | } 34 | 35 | @Override 36 | public void generate(byte[] salt) { 37 | random.nextBytes(salt); 38 | } 39 | 40 | private static SecureRandom createSecureRandom() { 41 | return new SecureRandom(); 42 | } 43 | 44 | private static SecureRandom createSecureRandom(String algorithm) { 45 | SecureRandom random; 46 | try { 47 | random = SecureRandom.getInstance(algorithm); 48 | } catch (NoSuchAlgorithmException e) { 49 | throw new Jargon2Exception(INSTANTIATION_FAILURE, e); 50 | } 51 | return random; 52 | } 53 | 54 | private static SecureRandom createSecureRandom(String algorithm, String provider) { 55 | SecureRandom random; 56 | try { 57 | random = SecureRandom.getInstance(algorithm, provider); 58 | } catch (NoSuchAlgorithmException | NoSuchProviderException e) { 59 | throw new Jargon2Exception(INSTANTIATION_FAILURE, e); 60 | } 61 | return random; 62 | } 63 | 64 | private static SecureRandom createSecureRandom(String algorithm, Provider provider) { 65 | SecureRandom random; 66 | try { 67 | random = SecureRandom.getInstance(algorithm, provider); 68 | } catch (NoSuchAlgorithmException e) { 69 | throw new Jargon2Exception(INSTANTIATION_FAILURE, e); 70 | } 71 | return random; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/kosprov/jargon2/internal/VerifierImpl.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.internal; 2 | 3 | import com.kosprov.jargon2.api.Jargon2Exception; 4 | import com.kosprov.jargon2.internal.discovery.Jargon2BackendDiscovery; 5 | import com.kosprov.jargon2.spi.Jargon2Backend; 6 | 7 | import java.util.Collections; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | import static com.kosprov.jargon2.api.Jargon2.*; 12 | 13 | public class VerifierImpl implements Verifier { 14 | Jargon2Backend backend = Jargon2BackendDiscovery.INSTANCE.getJargon2Backend(); 15 | Map options = Collections.emptyMap(); 16 | Type type = Type.ARGON2i; 17 | Version version = Version.V13; 18 | int timeCost = 3; 19 | int memoryCost = 4096; 20 | int lanes = 1; 21 | int threads = 1; 22 | boolean autoThreads = true; 23 | byte[] salt; 24 | byte[] password; 25 | byte[] secret; 26 | byte[] ad; 27 | String encodedHash; 28 | byte[] rawHash; 29 | 30 | public VerifierImpl() { 31 | } 32 | 33 | private VerifierImpl(VerifierImpl copy) { 34 | this.backend = copy.backend; 35 | this.options = copy.options; 36 | this.type = copy.type; 37 | this.version = copy.version; 38 | this.timeCost = copy.timeCost; 39 | this.memoryCost = copy.memoryCost; 40 | this.lanes = copy.lanes; 41 | this.threads = copy.threads; 42 | this.autoThreads = copy.autoThreads; 43 | this.salt = copy.salt; 44 | this.password = copy.password; 45 | this.secret = copy.secret; 46 | this.ad = copy.ad; 47 | 48 | this.encodedHash = copy.encodedHash; 49 | this.rawHash = copy.rawHash; 50 | } 51 | 52 | @Override 53 | public VerifierImpl backend(Jargon2Backend backend) { 54 | VerifierImpl copy = new VerifierImpl(this); 55 | copy.backend = backend; 56 | return copy; 57 | } 58 | 59 | @Override 60 | public VerifierImpl backend(String backendClass) { 61 | try { 62 | return backend(Class.forName(backendClass).asSubclass(Jargon2Backend.class)); 63 | } catch (Exception e) { 64 | throw new Jargon2Exception("Could not create Jargon2Backend instance from class " + backendClass); 65 | } 66 | } 67 | 68 | @Override 69 | public VerifierImpl backend(Class backendClass) { 70 | try { 71 | return backend(backendClass.newInstance()); 72 | } catch (Exception e) { 73 | throw new Jargon2Exception("Could not create Jargon2Backend instance from class " + backendClass); 74 | } 75 | } 76 | 77 | @Override 78 | public VerifierImpl options(Map options) { 79 | VerifierImpl copy = new VerifierImpl(this); 80 | copy.options = options != null ? new HashMap<>(options) : Collections.emptyMap(); 81 | return copy; 82 | } 83 | 84 | @Override 85 | public VerifierImpl type(Type type) { 86 | VerifierImpl copy = new VerifierImpl(this); 87 | copy.type = type; 88 | return copy; 89 | } 90 | 91 | @Override 92 | public VerifierImpl version(Version version) { 93 | VerifierImpl copy = new VerifierImpl(this); 94 | copy.version = version; 95 | return copy; 96 | } 97 | 98 | @Override 99 | public VerifierImpl timeCost(int timeCost) { 100 | VerifierImpl copy = new VerifierImpl(this); 101 | copy.timeCost = timeCost; 102 | return copy; 103 | } 104 | 105 | @Override 106 | public VerifierImpl memoryCost(int memoryCost) { 107 | VerifierImpl copy = new VerifierImpl(this); 108 | copy.memoryCost = memoryCost; 109 | return copy; 110 | } 111 | 112 | @Override 113 | public VerifierImpl parallelism(int parallelism) { 114 | VerifierImpl copy = new VerifierImpl(this); 115 | copy.lanes = parallelism; 116 | copy.threads = parallelism; 117 | copy.autoThreads = false; 118 | return copy; 119 | } 120 | 121 | @Override 122 | public VerifierImpl parallelism(int lanes, int threads) { 123 | VerifierImpl copy = new VerifierImpl(this); 124 | copy.lanes = lanes; 125 | copy.threads = threads; 126 | copy.autoThreads = false; 127 | return copy; 128 | } 129 | 130 | @Override 131 | public VerifierImpl threads(int threads) { 132 | VerifierImpl copy = new VerifierImpl(this); 133 | copy.threads = threads; 134 | copy.autoThreads = false; 135 | return copy; 136 | } 137 | 138 | @Override 139 | public VerifierImpl salt(byte[] salt) { 140 | VerifierImpl copy = new VerifierImpl(this); 141 | copy.salt = salt; 142 | return copy; 143 | } 144 | 145 | @Override 146 | public VerifierImpl salt(ByteArray salt) { 147 | VerifierImpl copy = new VerifierImpl(this); 148 | copy.salt = salt.getBytes(); 149 | return copy; 150 | } 151 | 152 | @Override 153 | public VerifierImpl password(byte[] password) { 154 | VerifierImpl copy = new VerifierImpl(this); 155 | copy.password = password; 156 | return copy; 157 | } 158 | 159 | @Override 160 | public VerifierImpl password(ByteArray password) { 161 | VerifierImpl copy = new VerifierImpl(this); 162 | copy.password = password.getBytes(); 163 | return copy; 164 | } 165 | 166 | @Override 167 | public VerifierImpl secret(byte[] secret) { 168 | VerifierImpl copy = new VerifierImpl(this); 169 | copy.secret = secret; 170 | return copy; 171 | } 172 | 173 | @Override 174 | public VerifierImpl secret(ByteArray secret) { 175 | VerifierImpl copy = new VerifierImpl(this); 176 | copy.secret = secret.getBytes(); 177 | return copy; 178 | } 179 | 180 | @Override 181 | public VerifierImpl ad(byte[] ad) { 182 | VerifierImpl copy = new VerifierImpl(this); 183 | copy.ad = ad; 184 | return copy; 185 | } 186 | 187 | @Override 188 | public VerifierImpl ad(ByteArray ad) { 189 | VerifierImpl copy = new VerifierImpl(this); 190 | copy.ad = ad.getBytes(); 191 | return copy; 192 | } 193 | 194 | @Override 195 | public EncodedVerifierImpl hash(String encodedHash) { 196 | VerifierImpl copy = new VerifierImpl(this); 197 | copy.encodedHash = encodedHash; 198 | copy.rawHash = null; 199 | return new EncodedVerifierImpl(copy); 200 | } 201 | 202 | @Override 203 | public RawVerifierImpl hash(byte[] rawHash) { 204 | VerifierImpl copy = new VerifierImpl(this); 205 | copy.rawHash = rawHash; 206 | copy.encodedHash = null; 207 | return new RawVerifierImpl(copy); 208 | } 209 | 210 | @Override 211 | public String toString() { 212 | // Careful not to leak any sensitive data 213 | return "Verifier{" + 214 | "backend=" + backend.getClass().getName() + 215 | ", options=" + options.size() + " item(s)" + 216 | ", type=" + type + 217 | ", version=" + version + 218 | ", timeCost=" + timeCost + 219 | ", memoryCost=" + memoryCost + 220 | ", lanes=" + lanes + 221 | ", threads=" + threads + 222 | '}'; 223 | } 224 | 225 | private static class EncodedVerifierImpl implements EncodedVerifier { 226 | private final VerifierImpl delegate; 227 | 228 | EncodedVerifierImpl(VerifierImpl verifier) { 229 | this.delegate = verifier; 230 | } 231 | 232 | @Override 233 | public EncodedVerifierImpl backend(Jargon2Backend backend) { 234 | return new EncodedVerifierImpl(delegate.backend(backend)); 235 | } 236 | 237 | @Override 238 | public EncodedVerifierImpl backend(String backendClass) { 239 | return new EncodedVerifierImpl(delegate.backend(backendClass)); 240 | } 241 | 242 | @Override 243 | public EncodedVerifierImpl backend(Class backendClass) { 244 | return new EncodedVerifierImpl(delegate.backend(backendClass)); 245 | } 246 | 247 | @Override 248 | public EncodedVerifierImpl options(Map options) { 249 | return new EncodedVerifierImpl(delegate.options(options)); 250 | } 251 | 252 | @Override 253 | public EncodedVerifierImpl type(Type type) { 254 | return new EncodedVerifierImpl(delegate.type(type)); 255 | } 256 | 257 | @Override 258 | public EncodedVerifierImpl version(Version version) { 259 | return new EncodedVerifierImpl(delegate.version(version)); 260 | } 261 | 262 | @Override 263 | public EncodedVerifierImpl timeCost(int timeCost) { 264 | return new EncodedVerifierImpl(delegate.timeCost(timeCost)); 265 | } 266 | 267 | @Override 268 | public EncodedVerifierImpl memoryCost(int memoryCost) { 269 | return new EncodedVerifierImpl(delegate.memoryCost(memoryCost)); 270 | } 271 | 272 | @Override 273 | public EncodedVerifierImpl parallelism(int parallelism) { 274 | return new EncodedVerifierImpl(delegate.parallelism(parallelism)); 275 | } 276 | 277 | @Override 278 | public EncodedVerifierImpl parallelism(int lanes, int threads) { 279 | return new EncodedVerifierImpl(delegate.parallelism(lanes, threads)); 280 | } 281 | 282 | @Override 283 | public EncodedVerifierImpl threads(int threads) { 284 | return new EncodedVerifierImpl(delegate.threads(threads)); 285 | } 286 | 287 | @Override 288 | public EncodedVerifierImpl salt(byte[] salt) { 289 | return new EncodedVerifierImpl(delegate.salt(salt)); 290 | } 291 | 292 | @Override 293 | public EncodedVerifierImpl salt(ByteArray salt) { 294 | return new EncodedVerifierImpl(delegate.salt(salt)); 295 | } 296 | 297 | @Override 298 | public EncodedVerifierImpl password(byte[] password) { 299 | return new EncodedVerifierImpl(delegate.password(password)); 300 | } 301 | 302 | @Override 303 | public EncodedVerifierImpl password(ByteArray password) { 304 | return new EncodedVerifierImpl(delegate.password(password)); 305 | } 306 | 307 | @Override 308 | public EncodedVerifierImpl secret(byte[] secret) { 309 | return new EncodedVerifierImpl(delegate.secret(secret)); 310 | } 311 | 312 | @Override 313 | public EncodedVerifierImpl secret(ByteArray secret) { 314 | return new EncodedVerifierImpl(delegate.secret(secret)); 315 | } 316 | 317 | @Override 318 | public EncodedVerifierImpl ad(byte[] ad) { 319 | return new EncodedVerifierImpl(delegate.ad(ad)); 320 | } 321 | 322 | @Override 323 | public EncodedVerifierImpl ad(ByteArray ad) { 324 | return new EncodedVerifierImpl(delegate.ad(ad)); 325 | } 326 | 327 | @Override 328 | public EncodedVerifierImpl hash(String encodedHash) { 329 | return delegate.hash(encodedHash); 330 | } 331 | 332 | @Override 333 | public RawVerifierImpl hash(byte[] rawHash) { 334 | return delegate.hash(rawHash); 335 | } 336 | 337 | @Override 338 | public boolean verifyEncoded() { 339 | if (delegate.autoThreads) { 340 | return new Jargon2BackendAdapter(delegate.backend).verifyEncoded( 341 | delegate.encodedHash, 342 | delegate.secret, 343 | delegate.ad, 344 | delegate.password, 345 | delegate.options 346 | ); 347 | } else { 348 | return new Jargon2BackendAdapter(delegate.backend).verifyEncoded( 349 | delegate.encodedHash, 350 | delegate.threads, 351 | delegate.secret, 352 | delegate.ad, 353 | delegate.password, 354 | delegate.options 355 | ); 356 | } 357 | } 358 | } 359 | 360 | private static class RawVerifierImpl implements RawVerifier { 361 | private final VerifierImpl delegate; 362 | 363 | RawVerifierImpl(VerifierImpl verifier) { 364 | this.delegate = verifier; 365 | } 366 | 367 | @Override 368 | public RawVerifierImpl backend(Jargon2Backend backend) { 369 | return new RawVerifierImpl(delegate.backend(backend)); 370 | } 371 | 372 | @Override 373 | public RawVerifierImpl backend(String backendClass) { 374 | return new RawVerifierImpl(delegate.backend(backendClass)); 375 | } 376 | 377 | @Override 378 | public RawVerifierImpl backend(Class backendClass) { 379 | return new RawVerifierImpl(delegate.backend(backendClass)); 380 | } 381 | 382 | @Override 383 | public RawVerifierImpl options(Map options) { 384 | return new RawVerifierImpl(delegate.options(options)); 385 | } 386 | 387 | @Override 388 | public RawVerifierImpl type(Type type) { 389 | return new RawVerifierImpl(delegate.type(type)); 390 | } 391 | 392 | @Override 393 | public RawVerifierImpl version(Version version) { 394 | return new RawVerifierImpl(delegate.version(version)); 395 | } 396 | 397 | @Override 398 | public RawVerifierImpl timeCost(int timeCost) { 399 | return new RawVerifierImpl(delegate.timeCost(timeCost)); 400 | } 401 | 402 | @Override 403 | public RawVerifierImpl memoryCost(int memoryCost) { 404 | return new RawVerifierImpl(delegate.memoryCost(memoryCost)); 405 | } 406 | 407 | @Override 408 | public RawVerifierImpl parallelism(int parallelism) { 409 | return new RawVerifierImpl(delegate.parallelism(parallelism)); 410 | } 411 | 412 | @Override 413 | public RawVerifierImpl parallelism(int lanes, int threads) { 414 | return new RawVerifierImpl(delegate.parallelism(lanes, threads)); 415 | } 416 | 417 | @Override 418 | public RawVerifierImpl threads(int threads) { 419 | return new RawVerifierImpl(delegate.threads(threads)); 420 | } 421 | 422 | @Override 423 | public RawVerifierImpl salt(byte[] salt) { 424 | return new RawVerifierImpl(delegate.salt(salt)); 425 | } 426 | 427 | @Override 428 | public RawVerifierImpl salt(ByteArray salt) { 429 | return new RawVerifierImpl(delegate.salt(salt)); 430 | } 431 | 432 | @Override 433 | public RawVerifierImpl password(byte[] password) { 434 | return new RawVerifierImpl(delegate.password(password)); 435 | } 436 | 437 | @Override 438 | public RawVerifierImpl password(ByteArray password) { 439 | return new RawVerifierImpl(delegate.password(password)); 440 | } 441 | 442 | @Override 443 | public RawVerifierImpl secret(byte[] secret) { 444 | return new RawVerifierImpl(delegate.secret(secret)); 445 | } 446 | 447 | @Override 448 | public RawVerifierImpl secret(ByteArray secret) { 449 | return new RawVerifierImpl(delegate.secret(secret)); 450 | } 451 | 452 | @Override 453 | public RawVerifierImpl ad(byte[] ad) { 454 | return new RawVerifierImpl(delegate.ad(ad)); 455 | } 456 | 457 | @Override 458 | public RawVerifierImpl ad(ByteArray ad) { 459 | return new RawVerifierImpl(delegate.ad(ad)); 460 | } 461 | 462 | @Override 463 | public EncodedVerifierImpl hash(String encodedHash) { 464 | return delegate.hash(encodedHash); 465 | } 466 | 467 | @Override 468 | public RawVerifierImpl hash(byte[] rawHash) { 469 | return delegate.hash(rawHash); 470 | } 471 | 472 | @Override 473 | public boolean verifyRaw() { 474 | int threads = delegate.autoThreads ? delegate.lanes : delegate.threads; 475 | return new Jargon2BackendAdapter(delegate.backend).verifyRaw( 476 | delegate.type, 477 | delegate.version, 478 | delegate.memoryCost, 479 | delegate.timeCost, 480 | delegate.lanes, threads, 481 | delegate.rawHash, 482 | delegate.secret, 483 | delegate.ad, 484 | delegate.salt, 485 | delegate.password, 486 | delegate.options 487 | ); 488 | } 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /src/main/java/com/kosprov/jargon2/internal/discovery/Jargon2BackendDiscovery.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.internal.discovery; 2 | 3 | import com.kosprov.jargon2.spi.Jargon2Backend; 4 | 5 | import java.util.HashSet; 6 | import java.util.ServiceLoader; 7 | import java.util.Set; 8 | 9 | /** 10 | * Singleton object that searches for {@link Jargon2Backend} implementations. 11 | * 12 | *

13 | * It checks the value of the -Dcom.kosprov.jargon2.spi.backend system property for the 14 | * class name of the Jargon2Backend implementation. This implementation must has a no-args constructor. 15 | *

16 | * 17 | *

18 | * Then, it checks for service providers registered with Java's {@link ServiceLoader} mechanism. 19 | * See {@link Jargon2Backend} documentation for more details. 20 | *

21 | * 22 | *

23 | * The previous 2 steps combined should output only 1 backend. 24 | *

25 | * 26 | * @see Jargon2Backend 27 | */ 28 | public enum Jargon2BackendDiscovery { 29 | 30 | INSTANCE; 31 | 32 | private static final String JARGON2_BACKEND_SYSTEM_PROP_NAME = "com.kosprov.jargon2.spi.backend"; 33 | 34 | private volatile Jargon2Backend backend; 35 | 36 | /** 37 | * Get the single instance of the {@link Jargon2Backend} 38 | * 39 | * @return The discovered backend instance 40 | */ 41 | public Jargon2Backend getJargon2Backend() { 42 | if (backend == null) { 43 | synchronized (this) { 44 | if (backend == null) { 45 | Set backendsFound = new HashSet<>(); 46 | 47 | String backendClassName = System.getProperty(JARGON2_BACKEND_SYSTEM_PROP_NAME); 48 | if (backendClassName != null && !"".equals(backendClassName.trim())) { 49 | try { 50 | backendsFound.add((Jargon2Backend) Class.forName(backendClassName).newInstance()); 51 | } catch (Exception e) { 52 | throw new Jargon2BackendDiscoveryException("Could not create Jargon2Backend instance from class " + backendClassName, e); 53 | } 54 | } 55 | 56 | ServiceLoader loader = ServiceLoader.load(Jargon2Backend.class); 57 | for (Jargon2Backend loadedBackend : loader) { 58 | backendsFound.add(loadedBackend); 59 | } 60 | 61 | if (backendsFound.size() == 1) { 62 | backend = backendsFound.iterator().next(); // All good 63 | } else if (backendsFound.size() > 1) { 64 | StringBuilder sb = new StringBuilder(); 65 | sb.append('['); 66 | for (Jargon2Backend b : backendsFound) { 67 | sb.append(b.getClass().getName()).append(", "); 68 | } 69 | sb.setLength(sb.length() - 2); 70 | sb.append(']'); 71 | throw new Jargon2BackendDiscoveryException("Found more than one Jargon2Backends: " + sb.toString()); 72 | } else { 73 | throw new Jargon2BackendDiscoveryException("Could not find appropriate jargon2Backend. Use either a service provider or define its class with -D" + JARGON2_BACKEND_SYSTEM_PROP_NAME); 74 | } 75 | } 76 | } 77 | } 78 | return backend; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/kosprov/jargon2/internal/discovery/Jargon2BackendDiscoveryException.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.internal.discovery; 2 | 3 | import com.kosprov.jargon2.api.Jargon2Exception; 4 | 5 | class Jargon2BackendDiscoveryException extends Jargon2Exception { 6 | 7 | Jargon2BackendDiscoveryException(String message) { 8 | super(message); 9 | } 10 | 11 | Jargon2BackendDiscoveryException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/kosprov/jargon2/spi/Jargon2Backend.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.spi; 2 | 3 | import com.kosprov.jargon2.api.Jargon2Exception; 4 | 5 | import java.util.Map; 6 | 7 | import static com.kosprov.jargon2.api.Jargon2.*; 8 | 9 | /** 10 | * Service Provider Interface (SPI) for Argon2 backend implementations. 11 | * 12 | *

13 | * Service registration can be done by one the two methods: i) Annotate the jar containing the implementation with a 14 | * {@link java.util.ServiceLoader} META-INF/services/com.kosprov.jargon2.spi.Jargon2Backend file 15 | * containing the fully qualified name of the implementation, or ii) by specifying the 16 | * -Dcom.kosprov.jargon2.spi.backend system property equal to the fully qualified name of the 17 | * implementation. 18 | *

19 | * 20 | *

21 | * Only one implementation must be found for the whole JVM by any of the discovery methods. 22 | *

23 | * 24 | *

25 | * Implementing classes must have a default constructor. 26 | *

27 | * 28 | */ 29 | public interface Jargon2Backend { 30 | 31 | /** 32 | *

Implementor's guides

33 | *

34 | * Implementors must validate input according to Argon2 specification. The basic validations are: minimum hashLength, 35 | * minimum salt length, minimum and maximum lanes, minimum and maximum threads, minimum memory cost and minimum time cost. 36 | *

37 | * 38 | *

39 | * Implementors must accept threads to be larger than lanes and use as many threads as lanes. 40 | *

41 | * 42 | * @param type The Argon2 {@link Type} 43 | * @param version The Argon2 {@link Version} 44 | * @param memoryCost The memory cost in kibi bytes (e.g. 65536 -> 64MB) 45 | * @param timeCost The number of passes through memory 46 | * @param lanes The number of memory lanes 47 | * @param threads The maximum number of threads to process lanes 48 | * @param hashLength The number of output bytes of the hash value 49 | * @param secret A secret for keyed hashing. Can be null 50 | * @param ad Additional authentication data to include into the hash. Can be null 51 | * @param salt The salt value to be used during hashing 52 | * @param password The password to be hashed 53 | * @param options Any options for the backend 54 | * @return A byte array of length hashLength with the hash value 55 | * @throws Jargon2Exception If required parameters are missing, are invalid or hash calculation fails unexpectedly 56 | * 57 | * @see LowLevelApi#rawHash(Type, Version, int, int, int, int, int, byte[], byte[], byte[], byte[], Map) 58 | */ 59 | byte[] rawHash(Type type, Version version, int memoryCost, int timeCost, int lanes, int threads, int hashLength, byte[] secret, byte[] ad, byte[] salt, byte[] password, Map options); 60 | 61 | /** 62 | *

Implementor's guides

63 | *

64 | * Implementors must validate input according to Argon2 specification. The basic validations are: minimum hashLength, 65 | * minimum salt length, minimum and maximum lanes, minimum and maximum threads, minimum memory cost and minimum time cost. 66 | *

67 | * 68 | *

69 | * Implementors must accept threads to be larger than lanes and use as many threads as lanes. 70 | *

71 | * 72 | * @param type The Argon2 {@link Type} 73 | * @param version The Argon2 {@link Version} 74 | * @param memoryCost The memory cost in kibi bytes (e.g. 65536 -> 64MB) 75 | * @param timeCost The number of passes through memory 76 | * @param lanes The number of memory lanes 77 | * @param threads The maximum number of threads to process lanes 78 | * @param hashLength The number of output bytes of the hash value 79 | * @param secret A secret for keyed hashing. Can be null 80 | * @param ad Additional authentication data to include into the hash. Can be null 81 | * @param salt The salt value to be used during hashing 82 | * @param password The password to be hashed 83 | * @param options Any options for the backend 84 | * @return A string containing the encoded hash value 85 | * @throws Jargon2Exception If required parameters are missing, are invalid or hash calculation fails unexpectedly 86 | * 87 | * @see LowLevelApi#encodedHash(Type, Version, int, int, int, int, int, byte[], byte[], byte[], byte[], Map) 88 | */ 89 | String encodedHash(Type type, Version version, int memoryCost, int timeCost, int lanes, int threads, int hashLength, byte[] secret, byte[] ad, byte[] salt, byte[] password, Map options); 90 | 91 | /** 92 | *

Implementor's guides

93 | *

94 | * Implementors must validate input according to Argon2 specification. The basic validations are: minimum raw hash length, 95 | * minimum salt length, minimum and maximum lanes, minimum and maximum threads, minimum memory cost and minimum time cost. 96 | *

97 | * 98 | *

99 | * Implementors must accept threads to be larger than lanes and use as many threads as lanes. 100 | *

101 | * 102 | * @param type The Argon2 {@link Type} 103 | * @param version The Argon2 {@link Version} 104 | * @param memoryCost The memory cost in kibi bytes (e.g. 65536 -> 64MB) 105 | * @param timeCost The number of passes through memory 106 | * @param lanes The number of memory lanes 107 | * @param threads The maximum number of threads to process lanes 108 | * @param rawHash The raw hash bytes 109 | * @param secret A secret for keyed hashing. Can be null 110 | * @param ad Additional authentication data to include into the hash. Can be null 111 | * @param salt The salt value to be used during hashing 112 | * @param password The password to be hashed 113 | * @param options Any options for the backend 114 | * @return true if recalculating the hash matches the given value 115 | * @throws Jargon2Exception If required parameters are missing, are invalid or verification fails unexpectedly 116 | * 117 | * @see LowLevelApi#verifyRaw(Type, Version, int, int, int, int, byte[], byte[], byte[], byte[], byte[], Map) 118 | */ 119 | boolean verifyRaw(Type type, Version version, int memoryCost, int timeCost, int lanes, int threads, byte[] rawHash, byte[] secret, byte[] ad, byte[] salt, byte[] password, Map options); 120 | 121 | /** 122 | *

Implementor's guides

123 | * 124 | *

125 | * Implementors must parse the encoded hash value and perform the same validations as {@link Jargon2Backend#verifyRaw}. 126 | *

127 | * 128 | * @param encodedHash The encoded hash 129 | * @param threads The maximum number of threads it be used during hash recalculation. -1 to derive the number of threads 130 | * from the parallelism property of the encoded hash. 131 | * @param secret The secret (keyed hashing) used during hashing. Can be null 132 | * @param ad Additional authentication data to included during hashing. Can be null 133 | * @param password The password to verify 134 | * @param options Any options for the backend 135 | * @return true if recalculating the hash matches the given value 136 | * @throws Jargon2Exception If required parameters are missing, are invalid or verification fails unexpectedly 137 | * 138 | * @see LowLevelApi#verifyEncoded(String, int, byte[], byte[], byte[], Map) 139 | */ 140 | boolean verifyEncoded(String encodedHash, int threads, byte[] secret, byte[] ad, byte[] password, Map options); 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/com/kosprov/jargon2/spi/Jargon2BackendException.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.spi; 2 | 3 | import com.kosprov.jargon2.api.Jargon2Exception; 4 | 5 | /** 6 | * Generic exception from a Jargon2 backend. 7 | */ 8 | public class Jargon2BackendException extends Jargon2Exception{ 9 | 10 | public Jargon2BackendException() { 11 | } 12 | 13 | public Jargon2BackendException(String message) { 14 | super(message); 15 | } 16 | 17 | public Jargon2BackendException(String message, Throwable cause) { 18 | super(message, cause); 19 | } 20 | 21 | public Jargon2BackendException(Throwable cause) { 22 | super(cause); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/com/kosprov/jargon2/api/CapturingDummyJargon2Backend.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.api; 2 | 3 | import java.util.Map; 4 | 5 | public class CapturingDummyJargon2Backend extends DummyJargon2Backend { 6 | 7 | public static class CapturedSet { 8 | public Jargon2.Type type; 9 | public Jargon2.Version version; 10 | public int memoryCost; 11 | public int timeCost; 12 | public int lanes; 13 | public int threads; 14 | public byte[] rawHash; 15 | public String encodedHash; 16 | public int hashLength; 17 | public byte[] secret; 18 | public byte[] ad; 19 | public byte[] salt; 20 | public byte[] password; 21 | public Map options; 22 | } 23 | 24 | public CapturedSet captured; 25 | 26 | @Override 27 | public byte[] rawHash(Jargon2.Type type, Jargon2.Version version, int memoryCost, int timeCost, int lanes, int threads, int hashLength, byte[] secret, byte[] ad, byte[] salt, byte[] password, Map options) { 28 | captured = new CapturedSet(); 29 | captured.type = type; 30 | captured.version = version; 31 | captured.memoryCost = memoryCost; 32 | captured.timeCost = timeCost; 33 | captured.lanes = lanes; 34 | captured.threads = threads; 35 | captured.hashLength = hashLength; 36 | captured.secret = secret; 37 | captured.ad = ad; 38 | captured.salt = salt; 39 | captured.password = password; 40 | captured.options = options; 41 | 42 | return super.rawHash(type, version, memoryCost, timeCost, lanes, threads, hashLength, secret, ad, salt, password, options); 43 | } 44 | 45 | @Override 46 | public String encodedHash(Jargon2.Type type, Jargon2.Version version, int memoryCost, int timeCost, int lanes, int threads, int hashLength, byte[] secret, byte[] ad, byte[] salt, byte[] password, Map options) { 47 | captured = new CapturedSet(); 48 | captured.type = type; 49 | captured.version = version; 50 | captured.memoryCost = memoryCost; 51 | captured.timeCost = timeCost; 52 | captured.lanes = lanes; 53 | captured.threads = threads; 54 | captured.hashLength = hashLength; 55 | captured.secret = secret; 56 | captured.ad = ad; 57 | captured.salt = salt; 58 | captured.password = password; 59 | captured.options = options; 60 | 61 | return super.encodedHash(type, version, memoryCost, timeCost, lanes, threads, hashLength, secret, ad, salt, password, options); 62 | } 63 | 64 | @Override 65 | public boolean verifyRaw(Jargon2.Type type, Jargon2.Version version, int memoryCost, int timeCost, int lanes, int threads, byte[] rawHash, byte[] secret, byte[] ad, byte[] salt, byte[] password, Map options) { 66 | captured = new CapturedSet(); 67 | captured.type = type; 68 | captured.version = version; 69 | captured.memoryCost = memoryCost; 70 | captured.timeCost = timeCost; 71 | captured.lanes = lanes; 72 | captured.threads = threads; 73 | captured.rawHash = rawHash; 74 | captured.secret = secret; 75 | captured.ad = ad; 76 | captured.salt = salt; 77 | captured.password = password; 78 | captured.options = options; 79 | 80 | return super.verifyRaw(type, version, memoryCost, timeCost, lanes, threads, rawHash, secret, ad, salt, password, options); 81 | } 82 | 83 | @Override 84 | public boolean verifyEncoded(String encodedHash, int threads, byte[] secret, byte[] ad, byte[] password, Map options) { 85 | captured = new CapturedSet(); 86 | captured.encodedHash = encodedHash; 87 | captured.threads = threads; 88 | captured.secret = secret; 89 | captured.ad = ad; 90 | captured.password = password; 91 | captured.options = options; 92 | 93 | return super.verifyEncoded(encodedHash, threads, secret, ad, password, options); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/com/kosprov/jargon2/api/DummyJargon2Backend.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.api; 2 | 3 | import com.kosprov.jargon2.spi.Jargon2Backend; 4 | import org.apache.commons.codec.binary.Base64; 5 | 6 | import java.util.Arrays; 7 | import java.util.Map; 8 | 9 | public class DummyJargon2Backend implements Jargon2Backend { 10 | 11 | @Override 12 | public byte[] rawHash(Jargon2.Type type, Jargon2.Version version, int memoryCost, int timeCost, int lanes, int threads, int hashLength, byte[] secret, byte[] ad, byte[] salt, byte[] password, Map options) { 13 | return doDummyHash(hashLength, password, salt, secret, ad); 14 | } 15 | 16 | @Override 17 | public String encodedHash(Jargon2.Type type, Jargon2.Version version, int memoryCost, int timeCost, int lanes, int threads, int hashLength, byte[] secret, byte[] ad, byte[] salt, byte[] password, Map options) { 18 | StringBuilder sb = new StringBuilder(); 19 | sb.append('$').append(type.getValue()); 20 | if (version.getValue() > Jargon2.Version.V10.getValue()) { 21 | sb.append('$').append("v=").append(version.getValue()); 22 | } 23 | sb.append('$').append("m=").append(memoryCost).append(",t=").append(timeCost).append(",p=").append(lanes); 24 | sb.append('$').append(encode(salt)); 25 | sb.append('$').append(encode(rawHash(type, version, memoryCost, timeCost, lanes, threads, hashLength, secret, ad, salt, password, options))); 26 | return sb.toString(); 27 | } 28 | 29 | @Override 30 | public boolean verifyRaw(Jargon2.Type type, Jargon2.Version version, int memoryCost, int timeCost, int lanes, int threads, byte[] rawHash, byte[] secret, byte[] ad, byte[] salt, byte[] password, Map options) { 31 | byte[] calcHash = doDummyHash(rawHash.length, password, salt, secret, ad); 32 | return Arrays.equals(rawHash, calcHash); 33 | } 34 | 35 | @Override 36 | public boolean verifyEncoded(String encodedHash, int threads, byte[] secret, byte[] ad, byte[] password, Map options) { 37 | int lastDollar = encodedHash.lastIndexOf('$'); 38 | byte[] hash = decode(encodedHash.substring(lastDollar + 1)); 39 | int lastLastDollar = encodedHash.lastIndexOf('$', lastDollar - 1); 40 | byte[] salt = decode(encodedHash.substring(lastLastDollar + 1, lastDollar)); 41 | byte[] calcHash = doDummyHash(hash.length, password, salt, secret, ad); 42 | return Arrays.equals(hash, calcHash); 43 | } 44 | 45 | private String encode(byte[] data) { 46 | return Base64.encodeBase64URLSafeString(data).replaceAll("-", "+").replaceAll("_", "/"); 47 | } 48 | 49 | private byte[] decode(String encoded) { 50 | return Base64.decodeBase64(encoded.replaceAll("\\+", "-").replaceAll("/", "_")); 51 | } 52 | 53 | private byte[] doDummyHash(int length, byte[]... data) { 54 | if (data == null || data.length == 0 || length == 0) return new byte[0]; 55 | 56 | byte[] hash = new byte[length]; 57 | 58 | int i = 0; 59 | for (byte[] d : data) { 60 | if (d != null) { 61 | for (byte b : d) { 62 | hash[i] = (byte) (hash[i] + 7 * b); 63 | i = (i + 1) % length; 64 | } 65 | } 66 | } 67 | 68 | return hash; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/com/kosprov/jargon2/api/DummyProvider.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.api; 2 | 3 | import java.security.Provider; 4 | import java.security.SecureRandomSpi; 5 | import java.util.*; 6 | 7 | public class DummyProvider extends Provider { 8 | 9 | private static final DummyProvider INSTANCE = new DummyProvider(); 10 | 11 | public static DummyProvider getInstance() { 12 | return INSTANCE; 13 | } 14 | 15 | private Map secureRandomServices = new HashMap() { 16 | { 17 | put("SHA1PRNG", new Service(DummyProvider.this, "SecureRandom", "SHA1PRNG", DummySha1PrngSecureRandomSpi.class.getName(), null, null)); 18 | put("NativePRNG", new Service(DummyProvider.this, "SecureRandom", "NativePRNG", DummyNativePrngSecureRandomSpi.class.getName(), null, null)); 19 | } 20 | }; 21 | 22 | private DummyProvider() { 23 | super("DUMMY", 1.0, "Dummy impl"); 24 | } 25 | 26 | @Override 27 | public synchronized Service getService(String type, String algorithm) { 28 | if ("SecureRandom".equals(type)) { 29 | return secureRandomServices.get(algorithm); 30 | } else { 31 | return null; 32 | } 33 | } 34 | 35 | @Override 36 | public synchronized Set getServices() { 37 | return new HashSet<>(secureRandomServices.values()); 38 | } 39 | 40 | public static class DummySha1PrngSecureRandomSpi extends SecureRandomSpi { 41 | static byte DUMMY_BYTE = (byte) 0b00000001; 42 | 43 | @Override 44 | protected void engineSetSeed(byte[] seed) { } 45 | 46 | @Override 47 | protected void engineNextBytes(byte[] bytes) { 48 | Arrays.fill(bytes, DUMMY_BYTE); 49 | } 50 | 51 | @Override 52 | protected byte[] engineGenerateSeed(int numBytes) { 53 | return new byte[numBytes]; 54 | } 55 | } 56 | 57 | public static class DummyNativePrngSecureRandomSpi extends SecureRandomSpi { 58 | static byte DUMMY_BYTE = (byte) 0b00000010; 59 | 60 | @Override 61 | protected void engineSetSeed(byte[] seed) { } 62 | 63 | @Override 64 | protected void engineNextBytes(byte[] bytes) { 65 | Arrays.fill(bytes, DUMMY_BYTE); 66 | } 67 | 68 | @Override 69 | protected byte[] engineGenerateSeed(int numBytes) { 70 | return new byte[numBytes]; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/com/kosprov/jargon2/api/DummySaltGenerator.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.api; 2 | 3 | import java.util.Arrays; 4 | 5 | class DummySaltGenerator implements Jargon2.SaltGenerator { 6 | 7 | static byte DUMMY_BYTE = (byte) 0b00000011; 8 | 9 | private static final DummySaltGenerator INSTANCE = new DummySaltGenerator(); 10 | 11 | static DummySaltGenerator getInstance() { 12 | return INSTANCE; 13 | } 14 | 15 | @Override 16 | public void generate(byte[] salt) { 17 | Arrays.fill(salt, DUMMY_BYTE ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/com/kosprov/jargon2/api/Jargon2Test.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.api; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.ByteArrayInputStream; 6 | import java.io.CharArrayReader; 7 | import java.io.InputStream; 8 | import java.io.InputStreamReader; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.Arrays; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | import static com.kosprov.jargon2.api.Jargon2.*; 15 | import static org.hamcrest.Matchers.equalTo; 16 | import static org.hamcrest.Matchers.is; 17 | import static org.hamcrest.Matchers.not; 18 | import static org.hamcrest.collection.IsMapWithSize.anEmptyMap; 19 | import static org.hamcrest.core.StringStartsWith.startsWith; 20 | import static org.junit.Assert.*; 21 | 22 | public class Jargon2Test { 23 | 24 | @Test 25 | public void typicalFluentApiTest() throws Exception { 26 | char[] reference = new char[] {'P', '@', 's', 's', 'W', '0', 'r', 'd'}; 27 | 28 | String hash; 29 | 30 | { 31 | char[] password = Arrays.copyOf(reference, reference.length); 32 | 33 | Hasher hasher = jargon2Hasher() 34 | .type(Type.ARGON2id) 35 | .memoryCost(4096) 36 | .timeCost(3) 37 | .parallelism(2) 38 | .saltLength(16) 39 | .hashLength(16); 40 | 41 | try (ByteArray passwordByteArray = toByteArray(password).clearSource()) { 42 | 43 | hash = hasher.password(passwordByteArray).encodedHash(); 44 | 45 | assertNotNull(hash); 46 | 47 | assertThat(reference, is(equalTo(password))); 48 | } 49 | 50 | for (char c : password) { 51 | assertEquals(0, c); 52 | } 53 | } 54 | 55 | { 56 | char[] password = Arrays.copyOf(reference, reference.length); 57 | 58 | boolean matches; 59 | 60 | try (ByteArray passwordByteArray = toByteArray(password).clearSource()) { 61 | matches = jargon2Verifier() 62 | .hash(hash) 63 | .password(passwordByteArray) 64 | .verifyEncoded(); 65 | 66 | assertTrue(matches); 67 | 68 | assertThat(reference, is(equalTo(password))); 69 | } 70 | 71 | for (char c : password) { 72 | assertEquals(0, c); 73 | } 74 | } 75 | } 76 | 77 | @Test 78 | public void allParamsPassedForEncodedHashing() { 79 | Type type = Type.ARGON2id; 80 | Version version = Version.V10; 81 | int memoryCost = 2048; 82 | int timeCost = 5; 83 | int lanes = 4; 84 | int threads = 2; 85 | int saltLength = 8; 86 | int hashLength = 8; 87 | 88 | byte[] password = "this is a password".getBytes(StandardCharsets.UTF_8); 89 | byte[] secret = "secret key".getBytes(StandardCharsets.UTF_8); 90 | byte[] ad = "some additional data".getBytes(StandardCharsets.UTF_8); 91 | 92 | Map options = new HashMap<>(); 93 | options.put("key1", 1); 94 | options.put("key2", 2); 95 | options.put("key3", 3); 96 | 97 | CapturingDummyJargon2Backend backend = new CapturingDummyJargon2Backend(); 98 | 99 | String hash = jargon2Hasher() 100 | .backend(backend) 101 | .options(options) 102 | .type(type) 103 | .version(version) 104 | .memoryCost(memoryCost) 105 | .timeCost(timeCost) 106 | .parallelism(lanes, threads) 107 | .saltLength(saltLength) 108 | .hashLength(hashLength) 109 | .secret(secret) 110 | .ad(ad) 111 | .password(password) 112 | .encodedHash(); 113 | 114 | assertEquals(type, backend.captured.type); 115 | assertEquals(version, backend.captured.version); 116 | assertEquals(memoryCost, backend.captured.memoryCost); 117 | assertEquals(timeCost, backend.captured.timeCost); 118 | assertEquals(lanes, backend.captured.lanes); 119 | assertEquals(threads, backend.captured.threads); 120 | assertEquals(hashLength, backend.captured.hashLength); 121 | assertSame(secret, backend.captured.secret); 122 | assertSame(ad, backend.captured.ad); 123 | assertSame(saltLength, backend.captured.salt.length); 124 | assertSame(password, backend.captured.password); 125 | assertEquals(options, backend.captured.options); 126 | 127 | assertNotNull(hash); 128 | assertThat(hash, startsWith("$argon2id$m=" + memoryCost + ",t=" + timeCost + ",p=" + lanes + "$")); 129 | 130 | threads = 1; 131 | 132 | boolean matches = jargon2Verifier() 133 | .backend(backend) 134 | .options(options) 135 | .hash(hash) 136 | .threads(threads) 137 | .secret(secret) 138 | .ad(ad) 139 | .password(password) 140 | .verifyEncoded(); 141 | 142 | assertSame(hash, backend.captured.encodedHash); 143 | assertEquals(threads, backend.captured.threads); 144 | assertSame(secret, backend.captured.secret); 145 | assertSame(ad, backend.captured.ad); 146 | assertSame(password, backend.captured.password); 147 | assertEquals(options, backend.captured.options); 148 | 149 | assertTrue(matches); 150 | } 151 | 152 | @Test 153 | public void noOptionsGiveEmptyOptionsToBackend() { 154 | CapturingDummyJargon2Backend backend = new CapturingDummyJargon2Backend(); 155 | 156 | byte[] password = "this is a password".getBytes(StandardCharsets.UTF_8); 157 | 158 | { 159 | String hash = jargon2Hasher() 160 | .backend(backend) 161 | .password(password) 162 | .encodedHash(); 163 | 164 | assertNotNull(backend.captured.options); 165 | assertThat(backend.captured.options, is(anEmptyMap())); 166 | 167 | boolean matches = jargon2Verifier() 168 | .backend(backend) 169 | .password(password) 170 | .hash(hash) 171 | .verifyEncoded(); 172 | 173 | assertNotNull(backend.captured.options); 174 | assertThat(backend.captured.options, is(anEmptyMap())); 175 | 176 | assertTrue(matches); 177 | } 178 | 179 | { 180 | String hash = jargon2Hasher() 181 | .backend(backend) 182 | .options(null) 183 | .password(password) 184 | .encodedHash(); 185 | 186 | assertNotNull(backend.captured.options); 187 | assertThat(backend.captured.options, is(anEmptyMap())); 188 | 189 | boolean matches = jargon2Verifier() 190 | .backend(backend) 191 | .options(null) 192 | .password(password) 193 | .hash(hash) 194 | .verifyEncoded(); 195 | 196 | assertNotNull(backend.captured.options); 197 | assertThat(backend.captured.options, is(anEmptyMap())); 198 | 199 | assertTrue(matches); 200 | } 201 | } 202 | 203 | @Test 204 | public void optionsAreCopiedToBackend() { 205 | CapturingDummyJargon2Backend backend = new CapturingDummyJargon2Backend(); 206 | 207 | byte[] password = "this is a password".getBytes(StandardCharsets.UTF_8); 208 | 209 | Map options = new HashMap<>(); 210 | options.put("key1", 1); 211 | options.put("key2", 2); 212 | options.put("key3", 3); 213 | 214 | String hash = jargon2Hasher() 215 | .backend(backend) 216 | .options(options) 217 | .password(password) 218 | .encodedHash(); 219 | 220 | assertNotNull(backend.captured.options); 221 | assertEquals(options, backend.captured.options); 222 | assertNotSame(options, backend.captured.options); 223 | 224 | boolean matches = jargon2Verifier() 225 | .backend(backend) 226 | .options(options) 227 | .password(password) 228 | .hash(hash) 229 | .verifyEncoded(); 230 | 231 | assertNotNull(backend.captured.options); 232 | assertEquals(options, backend.captured.options); 233 | assertNotSame(options, backend.captured.options); 234 | 235 | assertTrue(matches); 236 | } 237 | 238 | @Test 239 | public void customSaltGeneratorTest() { 240 | int saltLength = 8; 241 | int hashLength = 8; 242 | 243 | byte[] password = "this is a password".getBytes(StandardCharsets.UTF_8); 244 | 245 | CapturingDummyJargon2Backend backend = new CapturingDummyJargon2Backend(); 246 | 247 | Hasher hasher = jargon2Hasher() 248 | .backend(backend) 249 | .saltLength(saltLength) 250 | .hashLength(hashLength) 251 | .password(password); 252 | 253 | { 254 | String hash = hasher.saltGenerator("SHA1PRNG").encodedHash(); 255 | assertSame(saltLength, backend.captured.salt.length); 256 | assertNotNull(hash); 257 | } 258 | 259 | { 260 | String hash = hasher.saltGenerator("SHA1PRNG", DummyProvider.getInstance()).encodedHash(); 261 | assertEquals(saltLength, backend.captured.salt.length); 262 | for (byte b : backend.captured.salt) { 263 | assertEquals(DummyProvider.DummySha1PrngSecureRandomSpi.DUMMY_BYTE, b); 264 | } 265 | assertNotNull(hash); 266 | } 267 | 268 | { 269 | String hash = hasher.saltGenerator("NativePRNG", DummyProvider.getInstance()).encodedHash(); 270 | assertEquals(saltLength, backend.captured.salt.length); 271 | for (byte b : backend.captured.salt) { 272 | assertEquals(DummyProvider.DummyNativePrngSecureRandomSpi.DUMMY_BYTE, b); 273 | } 274 | assertNotNull(hash); 275 | } 276 | 277 | { 278 | String hash = hasher.saltGenerator(DummySaltGenerator.getInstance()).encodedHash(); 279 | assertEquals(saltLength, backend.captured.salt.length); 280 | for (byte b : backend.captured.salt) { 281 | assertEquals(DummySaltGenerator.DUMMY_BYTE, b); 282 | } 283 | assertNotNull(hash); 284 | } 285 | } 286 | 287 | @Test(expected = Jargon2Exception.class) 288 | public void erroneousSaltGeneratorTest() { 289 | jargon2Hasher().saltGenerator("WRONG"); 290 | } 291 | 292 | @Test(expected = Jargon2Exception.class) 293 | public void erroneousSaltGeneratorTest2() { 294 | jargon2Hasher().saltGenerator("WRONG", "WRONG"); 295 | } 296 | 297 | @Test(expected = Jargon2Exception.class) 298 | public void erroneousSaltGeneratorTest3() { 299 | jargon2Hasher().saltGenerator("WRONG", DummyProvider.getInstance()); 300 | } 301 | 302 | @Test 303 | public void allParamsPassedForRawHashing() { 304 | 305 | Type type = Type.ARGON2id; 306 | Version version = Version.V10; 307 | int memoryCost = 2048; 308 | int timeCost = 5; 309 | int lanes = 4; 310 | int threads = 2; 311 | int hashLength = 8; 312 | 313 | byte[] password = "this is a password".getBytes(StandardCharsets.UTF_8); 314 | byte[] salt = "some salt".getBytes(StandardCharsets.UTF_8); 315 | byte[] secret = "secret key".getBytes(StandardCharsets.UTF_8); 316 | byte[] ad = "some additional data".getBytes(StandardCharsets.UTF_8); 317 | 318 | Map options = new HashMap<>(); 319 | options.put("key1", 1); 320 | options.put("key2", 2); 321 | options.put("key3", 3); 322 | 323 | CapturingDummyJargon2Backend backend = new CapturingDummyJargon2Backend(); 324 | 325 | byte[] hash = jargon2Hasher() 326 | .backend(backend) 327 | .options(options) 328 | .type(type) 329 | .version(version) 330 | .memoryCost(memoryCost) 331 | .timeCost(timeCost) 332 | .parallelism(lanes, threads) 333 | .hashLength(hashLength) 334 | .salt(salt) 335 | .secret(secret) 336 | .ad(ad) 337 | .password(password) 338 | .rawHash(); 339 | 340 | assertEquals(type, backend.captured.type); 341 | assertEquals(version, backend.captured.version); 342 | assertEquals(memoryCost, backend.captured.memoryCost); 343 | assertEquals(timeCost, backend.captured.timeCost); 344 | assertEquals(lanes, backend.captured.lanes); 345 | assertEquals(threads, backend.captured.threads); 346 | assertEquals(hashLength, backend.captured.hashLength); 347 | assertSame(secret, backend.captured.secret); 348 | assertSame(ad, backend.captured.ad); 349 | assertSame(salt, backend.captured.salt); 350 | assertSame(password, backend.captured.password); 351 | assertEquals(options, backend.captured.options); 352 | 353 | assertNotNull(hash); 354 | 355 | threads = 1; 356 | 357 | boolean matches = jargon2Verifier() 358 | .backend(backend) 359 | .options(options) 360 | .type(type) 361 | .version(version) 362 | .memoryCost(memoryCost) 363 | .timeCost(timeCost) 364 | .parallelism(lanes) 365 | .parallelism(lanes, threads) // this should override threads to 1 366 | .hash(hash) 367 | .secret(secret) 368 | .ad(ad) 369 | .salt(salt) 370 | .password(password) 371 | .verifyRaw(); 372 | 373 | assertEquals(type, backend.captured.type); 374 | assertEquals(version, backend.captured.version); 375 | assertEquals(memoryCost, backend.captured.memoryCost); 376 | assertEquals(timeCost, backend.captured.timeCost); 377 | assertEquals(lanes, backend.captured.lanes); 378 | assertEquals(threads, backend.captured.threads); 379 | assertSame(hash, backend.captured.rawHash); 380 | assertSame(secret, backend.captured.secret); 381 | assertSame(ad, backend.captured.ad); 382 | assertSame(salt, backend.captured.salt); 383 | assertSame(password, backend.captured.password); 384 | assertEquals(options, backend.captured.options); 385 | 386 | assertTrue(matches); 387 | } 388 | 389 | @Test(expected = Jargon2Exception.class) 390 | public void noSaltForRawHashingTest() { 391 | byte[] password = "this is a password".getBytes(StandardCharsets.UTF_8); 392 | 393 | jargon2Hasher() 394 | .password(password) 395 | .rawHash(); 396 | } 397 | 398 | @Test 399 | public void lowLevelApiEncodedTest() { 400 | String password = "this is a password"; 401 | byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8); 402 | 403 | String salt = "some salt"; 404 | byte[] saltBytes = salt.getBytes(StandardCharsets.UTF_8); 405 | 406 | String encoded = jargon2LowLevelApi(DummyJargon2Backend.class.getName()).encodedHash( 407 | Type.ARGON2id, 408 | Version.V13, 409 | 4096, 410 | 3, 411 | 2, 412 | 16, 413 | saltBytes, 414 | passwordBytes 415 | ); 416 | 417 | boolean matches = jargon2LowLevelApi().verifyEncoded( 418 | encoded, 419 | passwordBytes 420 | ); 421 | 422 | assertTrue(matches); 423 | } 424 | 425 | @Test 426 | public void lowLevelApiEncodedAllParamsTest() { 427 | String secret = "this is a secret"; 428 | byte[] secretBytes = secret.getBytes(StandardCharsets.UTF_8); 429 | 430 | String ad = "this is additional data"; 431 | byte[] adBytes = ad.getBytes(StandardCharsets.UTF_8); 432 | 433 | String password = "this is a password"; 434 | byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8); 435 | 436 | String salt = "some salt"; 437 | byte[] saltBytes = salt.getBytes(StandardCharsets.UTF_8); 438 | 439 | String encoded = jargon2LowLevelApi(DummyJargon2Backend.class.getName()).encodedHash( 440 | Type.ARGON2id, 441 | Version.V13, 442 | 4096, 443 | 3, 444 | 2, 445 | 2, 446 | 16, 447 | secretBytes, 448 | adBytes, 449 | saltBytes, 450 | passwordBytes, 451 | null 452 | ); 453 | 454 | boolean matches = jargon2LowLevelApi().verifyEncoded( 455 | encoded, 456 | 2, 457 | secretBytes, 458 | adBytes, 459 | passwordBytes, 460 | null 461 | ); 462 | 463 | assertTrue(matches); 464 | } 465 | 466 | @Test 467 | public void lowLevelApiRawTest() { 468 | String password = "this is a password"; 469 | byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8); 470 | 471 | String salt = "some salt"; 472 | byte[] saltBytes = salt.getBytes(StandardCharsets.UTF_8); 473 | 474 | byte[] rawHash = jargon2LowLevelApi(DummyJargon2Backend.class.getName()).rawHash( 475 | Type.ARGON2id, 476 | Version.V13, 477 | 4096, 478 | 3, 479 | 2, 480 | 16, 481 | saltBytes, 482 | passwordBytes 483 | ); 484 | 485 | boolean matches = jargon2LowLevelApi().verifyRaw( 486 | Type.ARGON2id, 487 | Version.V13, 488 | 4096, 489 | 3, 490 | 2, 491 | rawHash, 492 | saltBytes, 493 | passwordBytes 494 | ); 495 | 496 | assertTrue(matches); 497 | } 498 | 499 | @Test 500 | public void lowLevelApiRawAllParamsTest() { 501 | String secret = "this is a secret"; 502 | byte[] secretBytes = secret.getBytes(StandardCharsets.UTF_8); 503 | 504 | String ad = "this is additional data"; 505 | byte[] adBytes = ad.getBytes(StandardCharsets.UTF_8); 506 | 507 | String password = "this is a password"; 508 | byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8); 509 | 510 | String salt = "some salt"; 511 | byte[] saltBytes = salt.getBytes(StandardCharsets.UTF_8); 512 | 513 | byte[] rawHash = jargon2LowLevelApi(DummyJargon2Backend.class.getName()).rawHash( 514 | Type.ARGON2id, 515 | Version.V13, 516 | 4096, 517 | 3, 518 | 2, 519 | 2, 520 | 16, 521 | secretBytes, 522 | adBytes, 523 | saltBytes, 524 | passwordBytes, 525 | null 526 | ); 527 | 528 | boolean matches = jargon2LowLevelApi().verifyRaw( 529 | Type.ARGON2id, 530 | Version.V13, 531 | 4096, 532 | 3, 533 | 2, 534 | 2, 535 | rawHash, 536 | secretBytes, 537 | adBytes, 538 | saltBytes, 539 | passwordBytes, 540 | null 541 | ); 542 | 543 | assertTrue(matches); 544 | } 545 | 546 | @Test(expected = Jargon2Exception.class) 547 | public void invalidBackendClassNameOnLowLevelApiTest() { 548 | jargon2LowLevelApi("invalid.class.Name"); 549 | } 550 | 551 | @Test(expected = Jargon2Exception.class) 552 | public void nonConstructableBackendClassNameOnLowLevelApiTest() { 553 | jargon2LowLevelApi(NonConstructableJargon2Backend.class.getName()); 554 | } 555 | 556 | @Test(expected = Jargon2Exception.class) 557 | public void nonConstructableBackendClassOnLowLevelApiTest() { 558 | jargon2LowLevelApi(NonConstructableJargon2Backend.class); 559 | } 560 | 561 | @Test(expected = Jargon2Exception.class) 562 | public void invalidBackendClassNameOnHasherTest() { 563 | jargon2Hasher().backend("invalid.class.Name"); 564 | } 565 | 566 | @Test(expected = Jargon2Exception.class) 567 | public void nonConstructableBackendClassNameOnHasherTest() { 568 | jargon2Hasher().backend(NonConstructableJargon2Backend.class.getName()); 569 | } 570 | 571 | @Test(expected = Jargon2Exception.class) 572 | public void nonConstructableBackendClassOnHasherTest() { 573 | jargon2Hasher().backend(NonConstructableJargon2Backend.class); 574 | } 575 | 576 | @Test(expected = Jargon2Exception.class) 577 | public void invalidBackendClassNameOnVerifierTest() { 578 | jargon2Verifier().backend("invalid.class.Name"); 579 | } 580 | 581 | @Test(expected = Jargon2Exception.class) 582 | public void nonConstructableBackendClassNameOnVerifierTest() { 583 | jargon2Verifier().backend(NonConstructableJargon2Backend.class.getName()); 584 | } 585 | 586 | @Test(expected = Jargon2Exception.class) 587 | public void nonConstructableBackendClassOnVerifierTest() { 588 | jargon2Verifier().backend(NonConstructableJargon2Backend.class); 589 | } 590 | 591 | @Test 592 | public void validBackendsTest() { 593 | jargon2LowLevelApi(DummyJargon2Backend.class.getName()); 594 | jargon2LowLevelApi(DummyJargon2Backend.class); 595 | jargon2LowLevelApi(new DummyJargon2Backend()); 596 | jargon2Hasher().backend(DummyJargon2Backend.class.getName()); 597 | jargon2Hasher().backend(DummyJargon2Backend.class); 598 | jargon2Hasher().backend(new DummyJargon2Backend()); 599 | jargon2Verifier().backend(DummyJargon2Backend.class.getName()); 600 | jargon2Verifier().backend(DummyJargon2Backend.class); 601 | jargon2Verifier().backend(new DummyJargon2Backend()); 602 | } 603 | 604 | @Test 605 | public void byteArraysTest() throws Exception { 606 | byte[] secret = "superSecret".getBytes(StandardCharsets.UTF_8); 607 | 608 | try (ByteArray secretByteArray = toByteArray(secret).clearSource()) { 609 | Hasher hasher = jargon2Hasher() 610 | .type(Type.ARGON2id) 611 | .memoryCost(8) 612 | .timeCost(1) 613 | .parallelism(1) 614 | .secret(secretByteArray) 615 | .hashLength(16); 616 | 617 | Verifier verifier = jargon2Verifier() 618 | .secret(secretByteArray); 619 | 620 | char[] ad = "additional data".toCharArray(); 621 | InputStreamReader salt = new InputStreamReader(new ByteArrayInputStream("this is a salt".getBytes())); 622 | InputStream password = new ByteArrayInputStream("this is a password".getBytes()); 623 | 624 | boolean matches; 625 | 626 | try (ByteArray adByteArray = toByteArray(ad).encoding("ASCII"); 627 | ByteArray saltByteArray = toByteArray(salt).encoding("ASCII"); 628 | ByteArray passwordByteArray = toByteArray(password)) { 629 | 630 | String hash = hasher 631 | .ad(adByteArray) 632 | .salt(saltByteArray) 633 | .password(passwordByteArray) 634 | .encodedHash(); 635 | 636 | matches = verifier 637 | .hash(hash) 638 | .ad(adByteArray) 639 | .password(passwordByteArray) 640 | .verifyEncoded(); 641 | } 642 | assertTrue(matches); 643 | } 644 | 645 | byte[] zeros = new byte[secret.length]; 646 | assertThat(zeros, is(equalTo(secret))); 647 | } 648 | 649 | @Test 650 | public void byteArrayEncodingTest() throws Exception { 651 | String str = "Φούμπαρ"; 652 | 653 | { 654 | CharSeqByteArray byteArray1 = toByteArray(str); 655 | byte[] bytes1 = byteArray1.getBytes(); 656 | 657 | ByteArray byteArray2 = byteArray1.encoding("ISO8859_7"); 658 | byte[] bytes2 = byteArray2.getBytes(); 659 | 660 | assertThat(bytes1, not(equalTo(bytes2))); 661 | assertThat(str.getBytes("ISO8859_7"), is(equalTo(bytes2))); 662 | } 663 | 664 | { 665 | char[] chars = str.toCharArray(); 666 | CharSeqByteArray byteArray1 = toByteArray(chars); 667 | byte[] bytes1 = byteArray1.getBytes(); 668 | 669 | ByteArray byteArray2 = byteArray1.encoding("ISO8859_7"); 670 | byte[] bytes2 = byteArray2.getBytes(); 671 | 672 | assertThat(bytes1, not(equalTo(bytes2))); 673 | assertThat(new String(chars).getBytes("ISO8859_7"), is(equalTo(bytes2))); 674 | } 675 | 676 | { 677 | char[] chars = str.toCharArray(); 678 | 679 | ByteArray byteArray1 = toByteArray(new CharArrayReader(chars)); 680 | byte[] bytes1 = byteArray1.getBytes(); 681 | 682 | ByteArray byteArray2 = toByteArray(new CharArrayReader(chars)).encoding("ISO8859_7"); 683 | byte[] bytes2 = byteArray2.getBytes(); 684 | 685 | assertThat(bytes1, not(equalTo(bytes2))); 686 | assertThat(new String(chars).getBytes("ISO8859_7"), is(equalTo(bytes2))); 687 | } 688 | } 689 | 690 | @Test 691 | public void byteArrayNormalizationTest() { 692 | 693 | Hasher hasher = jargon2Hasher() 694 | .type(Type.ARGON2id) 695 | .memoryCost(8) 696 | .timeCost(1) 697 | .parallelism(1) 698 | .saltLength(8) 699 | .hashLength(16); 700 | 701 | Verifier verifier = jargon2Verifier(); 702 | 703 | { 704 | String password1 = "\u00C1"; 705 | String password2 = "\u0041\u0301"; 706 | 707 | { 708 | String hash = hasher.password(toByteArray(password1)).encodedHash(); 709 | 710 | boolean matches = verifier.hash(hash).password(toByteArray(password2)).verifyEncoded(); 711 | assertFalse(matches); 712 | } 713 | 714 | { 715 | String hash = hasher.password(toByteArray(password1).normalize()).encodedHash(); 716 | 717 | boolean matches = verifier.hash(hash).password(toByteArray(password2).normalize()).verifyEncoded(); 718 | assertTrue(matches); 719 | } 720 | } 721 | 722 | { 723 | char[] password1 = new char[] { '\u00C1' }; 724 | char[] password2 = new char[] { '\u0041', '\u0301' }; 725 | { 726 | 727 | String hash = hasher.password(toByteArray(password1)).encodedHash(); 728 | 729 | boolean matches = verifier.hash(hash).password(toByteArray(password2)).verifyEncoded(); 730 | assertFalse(matches); 731 | } 732 | 733 | { 734 | String hash = hasher.password(toByteArray(password1).normalize()).encodedHash(); 735 | 736 | boolean matches = verifier.hash(hash).password(toByteArray(password2).normalize()).verifyEncoded(); 737 | assertTrue(matches); 738 | } 739 | } 740 | 741 | { 742 | char[] password1 = new char[] { '\u00C1' }; 743 | char[] password2 = new char[] { '\u0041', '\u0301' }; 744 | { 745 | 746 | String hash = hasher.password(toByteArray(new CharArrayReader(password1))).encodedHash(); 747 | 748 | boolean matches = verifier.hash(hash).password(toByteArray(new CharArrayReader(password2))).verifyEncoded(); 749 | assertFalse(matches); 750 | } 751 | 752 | { 753 | String hash = hasher.password(toByteArray(new CharArrayReader(password1)).normalize()).encodedHash(); 754 | 755 | boolean matches = verifier.hash(hash).password(toByteArray(new CharArrayReader(password2)).normalize()).verifyEncoded(); 756 | assertTrue(matches); 757 | } 758 | } 759 | } 760 | 761 | @Test 762 | public void propertiesMatchTest() { 763 | 764 | byte[] secret = "superSecret".getBytes(StandardCharsets.UTF_8); 765 | 766 | Hasher hasher = jargon2Hasher() 767 | .type(Type.ARGON2id) 768 | .memoryCost(8) 769 | .timeCost(1) 770 | .parallelism(1) 771 | .saltLength(8) 772 | .hashLength(16); 773 | 774 | String encodedHash = hasher.password(secret).encodedHash(); 775 | 776 | // encoded hash produced by this hasher must match 777 | assertTrue(hasher.propertiesMatch(encodedHash)); 778 | 779 | // encoded hash produced by this hasher must match again 780 | assertTrue(hasher.propertiesMatch(encodedHash)); 781 | 782 | encodedHash = "$argon2id$v=19$m=8,t=1,p=1$AAAAAAAAAAA$BBBBBBBBBBBBBBBBBBBBBB"; 783 | 784 | // build a new hasher by changing a single property 785 | // encoded hash must not match with the new hasher 786 | { 787 | Hasher otherHasher = hasher.type(Type.ARGON2i); 788 | assertFalse(otherHasher.propertiesMatch(encodedHash)); 789 | assertTrue(otherHasher.propertiesMatch(encodedHash.replace("argon2id", "argon2i"))); 790 | } 791 | 792 | { 793 | Hasher otherHasher = hasher.version(Version.V10); 794 | assertFalse(otherHasher.propertiesMatch(encodedHash)); 795 | assertTrue(otherHasher.propertiesMatch(encodedHash.replace("$v=19", ""))); 796 | } 797 | 798 | { 799 | Hasher otherHasher = hasher.memoryCost(16); 800 | assertFalse(otherHasher.propertiesMatch(encodedHash)); 801 | assertTrue(otherHasher.propertiesMatch(encodedHash.replace("m=8", "m=16"))); 802 | } 803 | 804 | { 805 | Hasher otherHasher = hasher.timeCost(10); 806 | assertFalse(otherHasher.propertiesMatch(encodedHash)); 807 | assertTrue(otherHasher.propertiesMatch(encodedHash.replace("t=1", "t=10"))); 808 | } 809 | 810 | { 811 | Hasher otherHasher = hasher.parallelism(4); 812 | assertFalse(otherHasher.propertiesMatch(encodedHash)); 813 | assertTrue(otherHasher.propertiesMatch(encodedHash.replace("p=1", "p=4"))); 814 | } 815 | 816 | { 817 | Hasher otherHasher = hasher.parallelism(4, 1); 818 | assertFalse(otherHasher.propertiesMatch(encodedHash)); 819 | assertTrue(otherHasher.propertiesMatch(encodedHash.replace("p=1", "p=4"))); 820 | } 821 | 822 | { 823 | Hasher otherHasher = hasher.saltLength(16); 824 | assertFalse(otherHasher.propertiesMatch(encodedHash)); 825 | assertTrue(otherHasher.propertiesMatch(encodedHash.replace("$AAAAAAAAAAA", "$AAAAAAAAAAAAAAAAAAAAAA"))); 826 | } 827 | 828 | { 829 | Hasher otherHasher = hasher.hashLength(32); 830 | assertFalse(otherHasher.propertiesMatch(encodedHash)); 831 | assertTrue(otherHasher.propertiesMatch(encodedHash.replace("$BBBBBBBBBBBBBBBBBBBBBB", "$BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"))); 832 | } 833 | 834 | // encoded hash produced by the original hasher must still match 835 | assertTrue(hasher.propertiesMatch(encodedHash)); 836 | 837 | // original hasher does not match with changed encoded hashes 838 | assertFalse(hasher.propertiesMatch(encodedHash.replace("argon2id", "argon2i"))); 839 | assertFalse(hasher.propertiesMatch(encodedHash.replace("$v=19", ""))); 840 | assertFalse(hasher.propertiesMatch(encodedHash.replace("m=8", "m=16"))); 841 | assertFalse(hasher.propertiesMatch(encodedHash.replace("t=1", "t=10"))); 842 | assertFalse(hasher.propertiesMatch(encodedHash.replace("p=1", "p=4"))); 843 | assertFalse(hasher.propertiesMatch(encodedHash.replace("p=1", "p=4"))); 844 | assertFalse(hasher.propertiesMatch(encodedHash.replace("$AAAAAAAAAAA", "$AAAAAAAAAAAAAAAAAAAAAA"))); 845 | assertFalse(hasher.propertiesMatch(encodedHash.replace("$BBBBBBBBBBBBBBBBBBBBBB", "$BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"))); 846 | } 847 | } -------------------------------------------------------------------------------- /src/test/java/com/kosprov/jargon2/api/NonConstructableJargon2Backend.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.api; 2 | 3 | import com.kosprov.jargon2.spi.Jargon2Backend; 4 | 5 | import java.util.Map; 6 | 7 | public class NonConstructableJargon2Backend implements Jargon2Backend { 8 | 9 | public NonConstructableJargon2Backend(String dummy) { 10 | } 11 | 12 | @Override 13 | public byte[] rawHash(Jargon2.Type type, Jargon2.Version version, int memoryCost, int timeCost, int lanes, int threads, int hashLength, byte[] secret, byte[] ad, byte[] salt, byte[] password, Map options) { 14 | return null; 15 | } 16 | 17 | @Override 18 | public String encodedHash(Jargon2.Type type, Jargon2.Version version, int memoryCost, int timeCost, int lanes, int threads, int hashLength, byte[] secret, byte[] ad, byte[] salt, byte[] password, Map options) { 19 | return null; 20 | } 21 | 22 | @Override 23 | public boolean verifyRaw(Jargon2.Type type, Jargon2.Version version, int memoryCost, int timeCost, int lanes, int threads, byte[] rawHash, byte[] secret, byte[] ad, byte[] salt, byte[] password, Map options) { 24 | return false; 25 | } 26 | 27 | @Override 28 | public boolean verifyEncoded(String encodedHash, int threads, byte[] secret, byte[] ad, byte[] password, Map options) { 29 | return false; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com/kosprov/jargon2/internal/ByteArrayImplTest.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.internal; 2 | 3 | import com.kosprov.jargon2.api.Jargon2; 4 | import org.junit.Test; 5 | 6 | import java.io.ByteArrayInputStream; 7 | import java.io.CharArrayReader; 8 | import java.nio.charset.StandardCharsets; 9 | import java.util.Arrays; 10 | 11 | import static com.kosprov.jargon2.api.Jargon2.ByteArray; 12 | import static org.junit.Assert.*; 13 | 14 | public class ByteArrayImplTest { 15 | 16 | @Test 17 | public void toByteArrayFromCharArrayTest() throws Exception { 18 | 19 | char[] chars = "0123456789".toCharArray(); 20 | 21 | ByteArray byteArray = new ByteArrayImpl.CharSeqByteArrayImpl(chars, StandardCharsets.UTF_8); 22 | byte[] bytes = byteArray.getBytes(); 23 | 24 | assertTrue(Arrays.equals(new String(chars).getBytes(StandardCharsets.UTF_8), bytes)); 25 | } 26 | 27 | @Test 28 | public void toByteArrayFromCharArrayNonAsciiTest() throws Exception { 29 | 30 | char[] chars = "Φούμπαρ".toCharArray(); 31 | 32 | ByteArray byteArray = new ByteArrayImpl.CharSeqByteArrayImpl(chars, StandardCharsets.UTF_8); 33 | byte[] bytes = byteArray.getBytes(); 34 | 35 | assertTrue(Arrays.equals(new String(chars).getBytes(StandardCharsets.UTF_8), bytes)); 36 | } 37 | 38 | @Test 39 | public void toByteArrayFromCharArrayNonAsciiNonDefaultEncodingTest() throws Exception { 40 | 41 | char[] chars = "Φούμπαρ".toCharArray(); 42 | 43 | ByteArray byteArray1 = new ByteArrayImpl.CharSeqByteArrayImpl(chars, StandardCharsets.UTF_8); 44 | byte[] bytes1 = byteArray1.getBytes(); 45 | 46 | ByteArray byteArray2 = new ByteArrayImpl.CharSeqByteArrayImpl(chars, StandardCharsets.UTF_8).encoding("ISO8859_7"); 47 | byte[] bytes2 = byteArray2.getBytes(); 48 | 49 | assertFalse(Arrays.equals(bytes1, bytes2)); 50 | assertTrue(Arrays.equals(new String(chars).getBytes("ISO8859_7"), bytes2)); 51 | } 52 | 53 | @Test 54 | public void toByteArrayFromStringTest() throws Exception { 55 | 56 | String str = "0123456789"; 57 | 58 | ByteArray byteArray = new ByteArrayImpl.CharSeqByteArrayImpl(str, StandardCharsets.UTF_8); 59 | byte[] bytes = byteArray.getBytes(); 60 | 61 | assertTrue(Arrays.equals(str.getBytes(StandardCharsets.UTF_8), bytes)); 62 | } 63 | 64 | @Test 65 | public void toByteArrayFromStringNonAsciiTest() throws Exception { 66 | 67 | String str = "Φούμπαρ"; 68 | 69 | ByteArray byteArray = new ByteArrayImpl.CharSeqByteArrayImpl(str, StandardCharsets.UTF_8); 70 | byte[] bytes = byteArray.getBytes(); 71 | 72 | assertTrue(Arrays.equals(str.getBytes(StandardCharsets.UTF_8), bytes)); 73 | } 74 | 75 | @Test 76 | public void toByteArrayFromStringNonAsciiNonDefaultEncodingTest() throws Exception { 77 | 78 | String str = "Φούμπαρ"; 79 | 80 | ByteArray byteArray1 = new ByteArrayImpl.CharSeqByteArrayImpl(str, StandardCharsets.UTF_8); 81 | byte[] bytes1 = byteArray1.getBytes(); 82 | 83 | ByteArray byteArray2 = new ByteArrayImpl.CharSeqByteArrayImpl(str, StandardCharsets.UTF_8).encoding("ISO8859_7"); 84 | byte[] bytes2 = byteArray2.getBytes(); 85 | 86 | assertFalse(Arrays.equals(bytes1, bytes2)); 87 | assertTrue(Arrays.equals(str.getBytes("ISO8859_7"), bytes2)); 88 | } 89 | 90 | @Test 91 | public void toByteArrayFromInputStreamTest() throws Exception { 92 | int bufferSize = 64; 93 | 94 | { 95 | // zero bytes 96 | byte[] bytes = new byte[0]; 97 | 98 | assertTrue(bytes.length == 0); 99 | 100 | ByteArray byteArray = new ByteArrayImpl(new ByteArrayInputStream(bytes), bufferSize); 101 | byte[] bytes2 = byteArray.getBytes(); 102 | 103 | assertTrue(bytes2.length == 0); 104 | } 105 | 106 | { 107 | // less than 64 bytes 108 | byte[] bytes = "0123456789".getBytes(); 109 | 110 | assertTrue(bytes.length < 64); 111 | 112 | ByteArray byteArray = new ByteArrayImpl(new ByteArrayInputStream(bytes), bufferSize); 113 | byte[] bytes2 = byteArray.getBytes(); 114 | 115 | assertTrue(Arrays.equals(bytes, bytes2)); 116 | } 117 | 118 | { 119 | // exactly 64 bytes 120 | byte[] bytes = "0123456789 0123456789 0123456789 0123456789 0123456789 012345678".getBytes(); 121 | 122 | assertTrue(bytes.length == 64); 123 | 124 | ByteArray byteArray = new ByteArrayImpl(new ByteArrayInputStream(bytes), bufferSize); 125 | byte[] bytes2 = byteArray.getBytes(); 126 | 127 | assertTrue(Arrays.equals(bytes, bytes2)); 128 | } 129 | 130 | { 131 | // less than 128 bytes 132 | byte[] bytes = "0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 01234".getBytes(); 133 | 134 | assertTrue(bytes.length > 64 && bytes.length < 128); 135 | 136 | ByteArray byteArray = new ByteArrayImpl(new ByteArrayInputStream(bytes), bufferSize); 137 | byte[] bytes2 = byteArray.getBytes(); 138 | 139 | assertTrue(Arrays.equals(bytes, bytes2)); 140 | } 141 | 142 | { 143 | // exactly 128 bytes 144 | byte[] bytes = "0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456".getBytes(); 145 | 146 | assertTrue(bytes.length == 128); 147 | 148 | ByteArray byteArray = new ByteArrayImpl(new ByteArrayInputStream(bytes), bufferSize); 149 | byte[] bytes2 = byteArray.getBytes(); 150 | 151 | assertTrue(Arrays.equals(bytes, bytes2)); 152 | } 153 | 154 | { 155 | // more than 128 bytes 156 | byte[] bytes = "0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789".getBytes(); 157 | 158 | assertTrue(bytes.length > 128); 159 | 160 | ByteArray byteArray = new ByteArrayImpl(new ByteArrayInputStream(bytes), bufferSize); 161 | byte[] bytes2 = byteArray.getBytes(); 162 | 163 | assertTrue(Arrays.equals(bytes, bytes2)); 164 | } 165 | } 166 | 167 | @Test 168 | public void toByteArrayFromReaderTest() throws Exception { 169 | char[] chars = "value".toCharArray(); 170 | 171 | ByteArray byteArray = new ByteArrayImpl.CharSeqByteArrayImpl(new CharArrayReader(chars), 64, StandardCharsets.UTF_8); 172 | byte[] bytes = byteArray.getBytes(); 173 | 174 | assertTrue(Arrays.equals(new String(chars).getBytes(StandardCharsets.UTF_8), bytes)); 175 | } 176 | 177 | @Test 178 | public void toByteArrayFromReaderNonAsciiTest() throws Exception { 179 | char[] chars = "Φούμπαρ".toCharArray(); 180 | 181 | ByteArray byteArray = new ByteArrayImpl.CharSeqByteArrayImpl(new CharArrayReader(chars), 64, StandardCharsets.UTF_8); 182 | byte[] bytes = byteArray.getBytes(); 183 | 184 | assertTrue(Arrays.equals(new String(chars).getBytes(StandardCharsets.UTF_8), bytes)); 185 | } 186 | 187 | @Test 188 | public void toByteArrayFromReaderNonAsciiNonDefaultEncodingTest() throws Exception { 189 | char[] chars = "Φούμπαρ".toCharArray(); 190 | 191 | ByteArray byteArray1 = new ByteArrayImpl.CharSeqByteArrayImpl(new CharArrayReader(chars), 64, StandardCharsets.UTF_8); 192 | byte[] bytes1 = byteArray1.getBytes(); 193 | 194 | ByteArray byteArray2 = new ByteArrayImpl.CharSeqByteArrayImpl(new CharArrayReader(chars), 64, StandardCharsets.UTF_8).encoding("ISO8859_7"); 195 | byte[] bytes2 = byteArray2.getBytes(); 196 | 197 | assertFalse(Arrays.equals(bytes1, bytes2)); 198 | assertTrue(Arrays.equals(new String(chars).getBytes("ISO8859_7"), bytes2)); 199 | } 200 | 201 | @Test 202 | public void toByteArrayFromCharArrayNormalizedTest() throws Exception { 203 | char[] chars1 = "\u00C1".toCharArray(); 204 | char[] chars2 = "\u0041\u0301".toCharArray(); 205 | 206 | { 207 | ByteArray byteArray1 = new ByteArrayImpl.CharSeqByteArrayImpl(chars1, StandardCharsets.UTF_8); 208 | byte[] bytes1 = byteArray1.getBytes(); 209 | 210 | ByteArray byteArray2 = new ByteArrayImpl.CharSeqByteArrayImpl(chars2, StandardCharsets.UTF_8); 211 | byte[] bytes2 = byteArray2.getBytes(); 212 | 213 | assertFalse(Arrays.equals(bytes1, bytes2)); 214 | } 215 | 216 | { 217 | ByteArray byteArray1 = new ByteArrayImpl.CharSeqByteArrayImpl(chars1, StandardCharsets.UTF_8).normalize(); 218 | byte[] bytes1 = byteArray1.getBytes(); 219 | 220 | ByteArray byteArray2 = new ByteArrayImpl.CharSeqByteArrayImpl(chars2, StandardCharsets.UTF_8).normalize(); 221 | byte[] bytes2 = byteArray2.getBytes(); 222 | 223 | assertTrue(Arrays.equals(bytes1, bytes2)); 224 | } 225 | } 226 | 227 | @Test 228 | public void toByteArrayFromStringNormalizedTest() throws Exception { 229 | String str1 = "\u00C1"; 230 | String str2 = "\u0041\u0301"; 231 | 232 | { 233 | ByteArray byteArray1 = new ByteArrayImpl.CharSeqByteArrayImpl(str1, StandardCharsets.UTF_8); 234 | byte[] bytes1 = byteArray1.getBytes(); 235 | 236 | ByteArray byteArray2 = new ByteArrayImpl.CharSeqByteArrayImpl(str2, StandardCharsets.UTF_8); 237 | byte[] bytes2 = byteArray2.getBytes(); 238 | 239 | assertFalse(Arrays.equals(bytes1, bytes2)); 240 | } 241 | 242 | { 243 | ByteArray byteArray1 = new ByteArrayImpl.CharSeqByteArrayImpl(str1, StandardCharsets.UTF_8).normalize(); 244 | byte[] bytes1 = byteArray1.getBytes(); 245 | 246 | ByteArray byteArray2 = new ByteArrayImpl.CharSeqByteArrayImpl(str2, StandardCharsets.UTF_8).normalize(); 247 | byte[] bytes2 = byteArray2.getBytes(); 248 | 249 | assertTrue(Arrays.equals(bytes1, bytes2)); 250 | } 251 | } 252 | 253 | @Test 254 | public void toByteArrayFromReaderNormalizedTest() throws Exception { 255 | char[] chars1 = "\u00C1".toCharArray(); 256 | char[] chars2 = "\u0041\u0301".toCharArray(); 257 | 258 | { 259 | ByteArray byteArray1 = new ByteArrayImpl.CharSeqByteArrayImpl(new CharArrayReader(chars1), 64, StandardCharsets.UTF_8); 260 | byte[] bytes1 = byteArray1.getBytes(); 261 | 262 | ByteArray byteArray2 = new ByteArrayImpl.CharSeqByteArrayImpl(new CharArrayReader(chars2), 64, StandardCharsets.UTF_8); 263 | byte[] bytes2 = byteArray2.getBytes(); 264 | 265 | assertFalse(Arrays.equals(bytes1, bytes2)); 266 | } 267 | 268 | { 269 | ByteArray byteArray1 = new ByteArrayImpl.CharSeqByteArrayImpl(new CharArrayReader(chars1), 64, StandardCharsets.UTF_8).normalize(); 270 | byte[] bytes1 = byteArray1.getBytes(); 271 | 272 | ByteArray byteArray2 = new ByteArrayImpl.CharSeqByteArrayImpl(new CharArrayReader(chars2), 64, StandardCharsets.UTF_8).normalize(); 273 | byte[] bytes2 = byteArray2.getBytes(); 274 | 275 | assertTrue(Arrays.equals(bytes1, bytes2)); 276 | } 277 | } 278 | 279 | @Test 280 | public void byteArrayClearSourceTest() throws Exception { 281 | byte[] referenceBytes = "12345".getBytes(); 282 | { 283 | byte[] source = Arrays.copyOf(referenceBytes, referenceBytes.length); 284 | byte[] copy; 285 | 286 | try (ByteArray byteArray = new ByteArrayImpl.ClearableSourceByteArrayImpl(source)) { 287 | copy = byteArray.getBytes(); 288 | 289 | assertTrue(Arrays.equals(source, copy)); 290 | } 291 | 292 | byte[] zeros = new byte[copy.length]; 293 | assertTrue(Arrays.equals(zeros, copy)); 294 | assertTrue(Arrays.equals(referenceBytes, source)); 295 | } 296 | 297 | { 298 | byte[] source = Arrays.copyOf(referenceBytes, referenceBytes.length); 299 | byte[] copy; 300 | 301 | try (ByteArray byteArray = new ByteArrayImpl.ClearableSourceByteArrayImpl(source).clearSource()) { 302 | copy = byteArray.getBytes(); 303 | 304 | assertTrue(Arrays.equals(source, copy)); 305 | } 306 | 307 | byte[] zeros = new byte[copy.length]; 308 | assertTrue(Arrays.equals(zeros, copy)); 309 | assertTrue(Arrays.equals(zeros, source)); 310 | } 311 | 312 | String referenceString = "Φούμπαρ"; 313 | 314 | { 315 | char[] source = referenceString.toCharArray(); 316 | byte[] copy; 317 | 318 | try(ByteArray byteArray = new ByteArrayImpl.ClearableSourceCharSeqByteArrayImpl(source, Jargon2.DEFAULT_ENCODING).encoding("ISO8859_7")) { 319 | copy = byteArray.getBytes(); 320 | 321 | assertTrue(Arrays.equals(new String(source).getBytes("ISO8859_7"), copy)); 322 | } 323 | 324 | byte[] zeros = new byte[copy.length]; 325 | assertTrue(Arrays.equals(zeros, copy)); 326 | assertTrue(Arrays.equals(referenceString.toCharArray(), source)); 327 | } 328 | 329 | { 330 | char[] source = referenceString.toCharArray(); 331 | byte[] copy; 332 | 333 | try(ByteArray byteArray = new ByteArrayImpl.ClearableSourceCharSeqByteArrayImpl(source, Jargon2.DEFAULT_ENCODING).encoding("ISO8859_7").clearSource()) { 334 | copy = byteArray.getBytes(); 335 | 336 | assertTrue(Arrays.equals(new String(source).getBytes("ISO8859_7"), copy)); 337 | } 338 | 339 | byte[] zeroBytes = new byte[copy.length]; 340 | assertTrue(Arrays.equals(zeroBytes, copy)); 341 | char[] zeroChars = new char[copy.length]; 342 | assertTrue(Arrays.equals(zeroChars, source)); 343 | } 344 | } 345 | 346 | @Test 347 | public void finalizableByteArrayTest() throws Exception { 348 | 349 | { 350 | byte[] bytes = new byte[] { 0x01 }; 351 | // non-finalizable: copyBytes stays as-is after GC 352 | byte[] copyBytes = new ByteArrayImpl(new ByteArrayInputStream(bytes), bytes.length).getBytes(); 353 | assertTrue(Arrays.equals(bytes, copyBytes)); 354 | System.gc(); 355 | System.runFinalization(); 356 | assertTrue(Arrays.equals(bytes, copyBytes)); 357 | } 358 | 359 | { 360 | byte[] bytes = new byte[] { 0x01 }; 361 | // finalizable: copyBytes are wiped out after GC 362 | byte[] copyBytes = new ByteArrayImpl(new ByteArrayInputStream(bytes), bytes.length).finalizable().getBytes(); 363 | assertTrue(Arrays.equals(bytes, copyBytes)); 364 | System.gc(); 365 | System.runFinalization(); 366 | assertFalse(Arrays.equals(bytes, copyBytes)); 367 | assertEquals(0x00, copyBytes[0]); 368 | } 369 | 370 | { 371 | byte[] bytes = new byte[] { 0x01 }; 372 | // non-finalizable: copyBytes and bytes stays as-is after GC 373 | byte[] copyBytes = new ByteArrayImpl.ClearableSourceByteArrayImpl(bytes).clearSource().getBytes(); 374 | assertTrue(Arrays.equals(bytes, copyBytes)); 375 | System.gc(); 376 | System.runFinalization(); 377 | assertTrue(Arrays.equals(bytes, copyBytes)); 378 | } 379 | 380 | { 381 | byte[] bytes = new byte[] { 0x01 }; 382 | // finalizable: copyBytes and bytes are wiped out after GC 383 | byte[] copyBytes = new ByteArrayImpl.ClearableSourceByteArrayImpl(bytes).clearSource().finalizable().getBytes(); 384 | assertTrue(Arrays.equals(bytes, copyBytes)); 385 | System.gc(); 386 | System.runFinalization(); 387 | assertEquals(0x00, bytes[0]); 388 | assertEquals(0x00, copyBytes[0]); 389 | } 390 | 391 | { 392 | char c = 'a'; 393 | byte b = (byte) c; 394 | char[] chars = new char[] { c }; 395 | // non-finalizable: copyBytes and chars stays as-is after GC 396 | byte[] copyBytes = new ByteArrayImpl.ClearableSourceCharSeqByteArrayImpl(chars, StandardCharsets.UTF_8).clearSource().getBytes(); 397 | assertEquals(c, chars[0]); 398 | assertEquals(b, copyBytes[0]); 399 | System.gc(); 400 | System.runFinalization(); 401 | assertEquals(c, chars[0]); 402 | assertEquals(b, copyBytes[0]); 403 | } 404 | 405 | { 406 | char c = 'a'; 407 | byte b = (byte) c; 408 | char[] chars = new char[] { c }; 409 | // finalizable: copyBytes and chars are wiped out after GC 410 | byte[] copyBytes = new ByteArrayImpl.ClearableSourceCharSeqByteArrayImpl(chars, StandardCharsets.UTF_8).clearSource().finalizable().getBytes(); 411 | assertEquals(c, chars[0]); 412 | assertEquals(b, copyBytes[0]); 413 | System.gc(); 414 | System.runFinalization(); 415 | assertEquals(0, chars[0]); 416 | assertEquals(0x00, copyBytes[0]); 417 | } 418 | } 419 | } -------------------------------------------------------------------------------- /src/test/java/com/kosprov/jargon2/internal/discovery/Jargon2BackendDiscoveryErrorTest.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.internal.discovery; 2 | 3 | import com.kosprov.jargon2.api.Jargon2; 4 | import com.kosprov.jargon2.api.Jargon2Exception; 5 | import com.kosprov.jargon2.spi.Jargon2Backend; 6 | import org.junit.Test; 7 | 8 | import java.util.Map; 9 | 10 | public class Jargon2BackendDiscoveryErrorTest { 11 | 12 | @Test(expected = Jargon2BackendDiscoveryException.class) 13 | public void errorOnMoreThanOneBackendTest() { 14 | System.setProperty("com.kosprov.jargon2.spi.backend", AnotherJargon2Backend.class.getName()); 15 | try { 16 | Jargon2BackendDiscovery.INSTANCE.getJargon2Backend(); 17 | } finally { 18 | System.setProperty("com.kosprov.jargon2.spi.backend", ""); 19 | } 20 | } 21 | 22 | @Test(expected = Jargon2BackendDiscoveryException.class) 23 | public void errorOnInvalidBackendTest() { 24 | System.setProperty("com.kosprov.jargon2.spi.backend", "invalid.backend.class.Name"); 25 | try { 26 | Jargon2BackendDiscovery.INSTANCE.getJargon2Backend(); 27 | } finally { 28 | System.setProperty("com.kosprov.jargon2.spi.backend", ""); 29 | } 30 | } 31 | 32 | public static class AnotherJargon2Backend implements Jargon2Backend { 33 | @Override 34 | public byte[] rawHash(Jargon2.Type type, Jargon2.Version version, int memoryCost, int timeCost, int lanes, int threads, int hashLength, byte[] secret, byte[] ad, byte[] salt, byte[] password, Map options) throws Jargon2Exception { 35 | return new byte[0]; 36 | } 37 | 38 | @Override 39 | public String encodedHash(Jargon2.Type type, Jargon2.Version version, int memoryCost, int timeCost, int lanes, int threads, int hashLength, byte[] secret, byte[] ad, byte[] salt, byte[] password, Map options) throws Jargon2Exception { 40 | return null; 41 | } 42 | 43 | @Override 44 | public boolean verifyRaw(Jargon2.Type type, Jargon2.Version version, int memoryCost, int timeCost, int lanes, int threads, byte[] rawHash, byte[] secret, byte[] ad, byte[] salt, byte[] password, Map options) throws Jargon2Exception { 45 | return false; 46 | } 47 | 48 | @Override 49 | public boolean verifyEncoded(String encodedHash, int threads, byte[] secret, byte[] ad, byte[] password, Map options) throws Jargon2Exception { 50 | return false; 51 | } 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/test/java/com/kosprov/jargon2/internal/discovery/Jargon2BackendDiscoveryTest.java: -------------------------------------------------------------------------------- 1 | package com.kosprov.jargon2.internal.discovery; 2 | 3 | import com.kosprov.jargon2.spi.Jargon2Backend; 4 | import org.junit.Test; 5 | 6 | import static org.junit.Assert.assertNotNull; 7 | 8 | public class Jargon2BackendDiscoveryTest { 9 | 10 | @Test 11 | public void defaultTest() { 12 | Jargon2Backend backend = Jargon2BackendDiscovery.INSTANCE.getJargon2Backend(); 13 | assertNotNull(backend); 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/test/resources/META-INF/services/com.kosprov.jargon2.spi.Jargon2Backend: -------------------------------------------------------------------------------- 1 | com.kosprov.jargon2.api.DummyJargon2Backend --------------------------------------------------------------------------------