├── src ├── test │ ├── my │ │ └── custom │ │ │ └── path │ │ │ └── to │ │ │ └── some.properties │ ├── resources │ │ └── psw4j.properties │ ├── com │ │ └── password4j │ │ │ ├── PepperGeneratorTest.java │ │ │ ├── HashBuilderTest.java │ │ │ ├── HashTest.java │ │ │ ├── SaltGeneratorTest.java │ │ │ ├── Blake2bTest.java │ │ │ ├── PropertyReaderTest.java │ │ │ ├── SystemCheckTest.java │ │ │ ├── BalloonHashingFunctionTest.java │ │ │ ├── StringTest.java │ │ │ ├── ScryptFunctionTest.java │ │ │ └── IssuesTest.java │ └── org │ │ └── example │ │ └── project │ │ └── PublicPasswordTest.java └── main │ └── java │ └── com │ └── password4j │ ├── SaltOption.java │ ├── types │ ├── Bcrypt.java │ ├── Argon2.java │ └── Hmac.java │ ├── BenchmarkResult.java │ ├── BadParametersException.java │ ├── HashUpdate.java │ ├── PepperGenerator.java │ ├── SaltGenerator.java │ ├── SecureString.java │ ├── AbstractHashingFunction.java │ ├── PropertyReader.java │ ├── Password.java │ ├── MessageDigestFunction.java │ ├── CompressedPBKDF2Function.java │ ├── PBKDF2Function.java │ ├── Blake2b.java │ ├── HashBuilder.java │ ├── HashChecker.java │ └── BalloonHashingFunction.java ├── .github ├── ISSUE_TEMPLATE │ ├── custom.md │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── build.yml │ └── codeql-analysis.yml ├── CITATION.cff ├── .gitignore ├── examples ├── Service.java └── MigrateFromMD5.java ├── SECURITY.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── pom.xml └── CHANGELOG.md /src/test/my/custom/path/to/some.properties: -------------------------------------------------------------------------------- 1 | check.this.out=hello!! 2 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/SaltOption.java: -------------------------------------------------------------------------------- 1 | package com.password4j; 2 | 3 | public enum SaltOption 4 | { 5 | PREPEND, APPEND; 6 | } 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: "Bertoldi" 5 | given-names: "David" 6 | orcid: "https://orcid.org/0009-0006-8810-6062" 7 | title: "password4j" 8 | version: 1.8.4 9 | doi: 10.5281/zenodo.15778340 10 | date-released: 2025-06-30 11 | url: "https://github.com/Password4j/password4j" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 11 | .mvn/wrapper/maven-wrapper.jar 12 | 13 | .DS_Store 14 | .idea 15 | *.iml 16 | *.orig 17 | [Dd]esktop.ini 18 | Thumbs.db 19 | Thumbs.db:encryptable 20 | ehthumbs.db 21 | ehthumbs_vista.db 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Describe here the steps to reproduce the behavior 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Environment:** 20 | - OS: [e.g. iOS] 21 | - JDK [e.g. OracleJDK 12] 22 | - Version [e.g. 1.2.0] 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "maven" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | labels: 13 | - "priority: low" 14 | - "type: enhancement" 15 | - "status: confirmed" 16 | 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/test/resources/psw4j.properties: -------------------------------------------------------------------------------- 1 | global.random.strong=false 2 | global.banner=true 3 | global.pepper=AlicePepper 4 | global.salt.length=16 5 | 6 | hash.md.algorithm=SHA-512 7 | hash.md.salt.option=append 8 | 9 | hash.pbkdf2.algorithm=SHA256 10 | hash.pbkdf2.iterations=64000 11 | hash.pbkdf2.length=256 12 | hash.pbkdf2.delimiter=$ 13 | 14 | hash.bcrypt.minor=b 15 | hash.bcrypt.rounds=12 16 | 17 | hash.scrypt.workfactor=16384 18 | hash.scrypt.resources=16 19 | hash.scrypt.parallelization=1 20 | 21 | hash.argon2.memory=1024 22 | hash.argon2.iterations=5 23 | hash.argon2.length=64 24 | hash.argon2.parallelism=3 25 | hash.argon2.type=id 26 | hash.argon2.version=19 27 | 28 | test.int=10 29 | test.string=This is a string 30 | test.bool=true 31 | test.char=\\ -------------------------------------------------------------------------------- /examples/Service.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2022 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | 19 | public class Service 20 | { 21 | 22 | static String getPasswordFromDatabaseForCurrentUser() 23 | { 24 | return "8a0bee289129817329727eb05f9a81a5"; 25 | } 26 | 27 | static void storePasswordInDatabase(String hash) 28 | { 29 | return; 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/types/Bcrypt.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2021 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.password4j.types; 19 | 20 | public enum Bcrypt 21 | { 22 | A, B, X, Y; 23 | 24 | public static Bcrypt valueOf(char minor) 25 | { 26 | for (Bcrypt type : Bcrypt.values()) 27 | { 28 | if (type.minor() == minor) 29 | { 30 | return type; 31 | } 32 | } 33 | return null; 34 | } 35 | 36 | public char minor() 37 | { 38 | return name().toLowerCase().charAt(0); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/BenchmarkResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package com.password4j; 18 | 19 | public class BenchmarkResult

20 | { 21 | 22 | private final P prototype; 23 | 24 | private final long elapsed; 25 | 26 | BenchmarkResult(P prototype, long elapsed) 27 | { 28 | this.prototype = prototype; 29 | this.elapsed = elapsed; 30 | } 31 | 32 | public P getPrototype() 33 | { 34 | return prototype; 35 | } 36 | 37 | public long getElapsed() 38 | { 39 | return elapsed; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We release patches for security vulnerabilities. Which versions are eligible receiving such patches depend on the [CVSS 3](https://www.first.org/cvss/calculator/3.0) Rating: 6 | | Version | Supported | 7 | |---------|----------------| 8 | | 0.x | :red_circle: | 9 | | 1.0.x - 1.4.x | :red_circle: | 10 | | 1.5.x + | :green_circle: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | Please report (suspected) security vulnerabilities with the [Private Vulnerability Reporting](https://github.com/Password4j/password4j/security/advisories/new) functionality. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days. 15 | 16 | We generally **aren’t** interested in the following problems: 17 | * Any vulnerability with a [CVSS 3](https://www.first.org/cvss/calculator/3.0) score lower than `4.0`, unless it can be combined with other vulnerabilities to achieve a higher score. 18 | * DoS, phishing, text injection, or social engineering attacks. Wikis, Tracs, forums, etc are intended to allow users to edit them. 19 | * Output from automated scans - please manually verify issues and include a valid proof of concept. 20 | * Theoretical vulnerabilities where you can't demonstrate a significant security impact with a PoC. 21 | -------------------------------------------------------------------------------- /src/test/com/password4j/PepperGeneratorTest.java: -------------------------------------------------------------------------------- 1 | package com.password4j; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | 7 | public class PepperGeneratorTest 8 | { 9 | 10 | @Test 11 | public void testSaltLength() 12 | { 13 | // GIVEN 14 | int length = 23; 15 | 16 | // WHEN 17 | String pepper = PepperGenerator.generate(length); 18 | 19 | // THEN 20 | Assert.assertNotNull(pepper); 21 | Assert.assertEquals(length, pepper.length()); 22 | } 23 | 24 | 25 | @Test 26 | public void testSaltNoLength() 27 | { 28 | // GIVEN 29 | 30 | // WHEN 31 | String pepper = PepperGenerator.generate(); 32 | 33 | // THEN 34 | Assert.assertNotNull(pepper); 35 | Assert.assertEquals(24, pepper.length()); 36 | } 37 | 38 | @Test(expected = BadParametersException.class) 39 | public void testSaltNegativeLength() 40 | { 41 | // GIVEN 42 | 43 | // WHEN 44 | PepperGenerator.generate(-3); 45 | 46 | // THEN 47 | 48 | } 49 | 50 | @Test 51 | public void testSaltZeroLength() 52 | { 53 | // GIVEN 54 | int length = 0; 55 | 56 | // WHEN 57 | String pepper = PepperGenerator.generate(length); 58 | 59 | // THEN 60 | Assert.assertNotNull(pepper); 61 | Assert.assertEquals(length, pepper.length()); 62 | } 63 | 64 | @Test 65 | public void testAlice() 66 | { 67 | // GIVEN 68 | 69 | // WHEN 70 | String pepper = PepperGenerator.get(); 71 | 72 | // THEN 73 | Assert.assertEquals("AlicePepper", pepper); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/com/password4j/HashBuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.password4j; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | import java.awt.event.KeyEvent; 7 | 8 | 9 | public class HashBuilderTest 10 | { 11 | @Test 12 | public void testPrintable() 13 | { 14 | // GIVEN 15 | 16 | // WHEN 17 | Hash hash = Password.hash("a password").addRandomSalt().withCompressedPBKDF2(); 18 | 19 | // THEN 20 | Assert.assertNotNull(hash.getSalt()); 21 | Assert.assertTrue(isPrintableChar(hash.getResult())); 22 | } 23 | 24 | @Test 25 | public void testPrintable2() 26 | { 27 | // GIVEN 28 | 29 | // WHEN 30 | Hash hash = Password.hash("a password").withBcrypt(); 31 | 32 | // THEN 33 | Assert.assertNotNull(hash.getSalt()); 34 | Assert.assertTrue(isPrintableChar(hash.getResult())); 35 | } 36 | 37 | @Test 38 | public void testPrintable3() 39 | { 40 | // GIVEN 41 | 42 | // WHEN 43 | Hash hash = Password.hash("a password").addRandomSalt().withScrypt(); 44 | 45 | // THEN 46 | Assert.assertNotNull(hash.getSalt()); 47 | Assert.assertTrue(isPrintableChar(hash.getResult())); 48 | } 49 | 50 | private static boolean isPrintableChar(String str) 51 | { 52 | boolean res = true; 53 | for (char c : str.toCharArray()) 54 | { 55 | Character.UnicodeBlock block = Character.UnicodeBlock.of(c); 56 | res &= (!Character.isISOControl( 57 | c)) && c != KeyEvent.CHAR_UNDEFINED && block != null && block != Character.UnicodeBlock.SPECIALS; 58 | } 59 | 60 | return res; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | java-version: [ 8, 11, 17 ] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions/setup-java@v1 21 | with: 22 | java-version: ${{ matrix.java-version }} 23 | - run: mvn clean animal-sniffer:check install test 24 | 25 | analyze: 26 | runs-on: ubuntu-latest 27 | env: 28 | JAVA_TOOL_OPTIONS: "--add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED" 29 | MAVEN_OPTS: "--add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED" 30 | steps: 31 | - uses: actions/checkout@v2 32 | with: 33 | fetch-depth: 0 34 | - name: Set up JDK 17 35 | uses: actions/setup-java@v1 36 | with: 37 | java-version: 17 38 | - name: Cache SonarCloud packages 39 | uses: actions/cache@v4 40 | with: 41 | path: ~/.sonar/cache 42 | key: ${{ runner.os }}-sonar 43 | restore-keys: ${{ runner.os }}-sonar 44 | - name: Cache Maven packages 45 | uses: actions/cache@v4 46 | with: 47 | path: ~/.m2 48 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 49 | restore-keys: ${{ runner.os }}-m2 50 | - name: Build and analyze 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 54 | run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=Password4j_password4j 55 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/types/Argon2.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2021 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package com.password4j.types; 18 | 19 | /** 20 | * Enum containing the different variations of Argon2. 21 | * 22 | * @author David Bertoldi 23 | * @see Argon2 24 | * @since 1.5.0 25 | */ 26 | public enum Argon2 27 | { 28 | /** 29 | * It maximizes resistance to GPU cracking attacks. 30 | * It accesses the memory array in a password dependent order, which reduces the possibility of time–memory trade-off (TMTO) attacks, 31 | * but introduces possible side-channel attacks 32 | */ 33 | D, 34 | 35 | /** 36 | * It is optimized to resist side-channel attacks. It accesses the memory array in a password independent order. 37 | */ 38 | I, 39 | 40 | /** 41 | * It is a hybrid version. It follows the Argon2i approach for the first half pass over memory and the Argon2d approach for subsequent passes. 42 | * It is recommended to use Argon2id except when there are reasons to prefer one of the other two modes. 43 | */ 44 | ID; 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/BadParametersException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package com.password4j; 18 | 19 | /** 20 | * This exception is normally thrown when a not well formed parameter 21 | * is passed as argument to a function. 22 | *

23 | * This exception covers all the exceptions raised by underlying logic, 24 | * grouping them as one exception. 25 | * 26 | * @author David Bertoldi 27 | * @since 0.1.0 28 | */ 29 | public class BadParametersException extends IllegalArgumentException 30 | { 31 | 32 | private static final long serialVersionUID = 9204720180786210237L; 33 | 34 | /** 35 | * Constructs the exception. 36 | * 37 | * @param message the message describing the cause of the exception 38 | * @since 0.1.0 39 | */ 40 | public BadParametersException(String message) 41 | { 42 | super(message); 43 | } 44 | 45 | /** 46 | * Constructs the exception. 47 | * 48 | * @param message the message describing the cause of the exception 49 | * @param exception the exception masked by this object 50 | * @since 0.1.0 51 | */ 52 | public BadParametersException(String message, Throwable exception) 53 | { 54 | super(message, exception); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 2 * * 4' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: ['java'] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | 45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 46 | # If this step fails, then you should remove it and run the build manually (see below) 47 | - name: Autobuild 48 | uses: github/codeql-action/autobuild@v1 49 | 50 | # ℹ️ Command-line programs to run using the OS shell. 51 | # 📚 https://git.io/JvXDl 52 | 53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 54 | # and modify them (or add more) to build your code if your project 55 | # uses a compiled language 56 | 57 | #- run: | 58 | # make bootstrap 59 | # make release 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v1 63 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/types/Hmac.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2021 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.password4j.types; 19 | 20 | import com.password4j.CompressedPBKDF2Function; 21 | 22 | 23 | /** 24 | * Static representation of the commonly supported 25 | * Hmac variants. 26 | */ 27 | public enum Hmac 28 | { 29 | 30 | SHA1(160, 1), // 31 | SHA224(224, 2), // 32 | SHA256(256, 3), // 33 | SHA384(384, 4), // 34 | SHA512(512, 5); 35 | 36 | private final int bits; 37 | 38 | private final int code; 39 | 40 | /** 41 | * @param bits length of the produced hash 42 | * @param code uid used by {@link CompressedPBKDF2Function} 43 | */ 44 | Hmac(int bits, int code) 45 | { 46 | this.bits = bits; 47 | this.code = code; 48 | } 49 | 50 | /** 51 | * Finds the enum associated with the given code 52 | * 53 | * @param code a numeric uid that identifies the algorithm 54 | * @return a {@link Hmac} enum. Null if the code is not present in this enum 55 | */ 56 | public static Hmac fromCode(int code) 57 | { 58 | for (Hmac alg : values()) 59 | { 60 | if (alg.code() == code) 61 | { 62 | return alg; 63 | } 64 | } 65 | return null; 66 | } 67 | 68 | /** 69 | * @return length of the algorithm in bits 70 | */ 71 | public int bits() 72 | { 73 | return bits; 74 | } 75 | 76 | /** 77 | * @return the numeric uid used in {@link CompressedPBKDF2Function} 78 | */ 79 | public int code() 80 | { 81 | return code; 82 | } 83 | 84 | @Override 85 | public String toString() 86 | { 87 | return "PBKDF2WithHmac" + this.name(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/HashUpdate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.password4j; 19 | 20 | /** 21 | * This class represents the result of the password verification process 22 | * along with the generated {@link Hash}. 23 | *

24 | * If the verification passed a new hash is always provided. 25 | * 26 | * @author David Bertoldi 27 | * @since 1.3.0 28 | */ 29 | public class HashUpdate 30 | { 31 | public static final HashUpdate UNVERIFIED = new HashUpdate(); 32 | 33 | private Hash hash; 34 | 35 | private boolean updated; 36 | 37 | private HashUpdate() 38 | { 39 | // 40 | } 41 | 42 | /** 43 | * @param hash the new hash 44 | * @throws BadParametersException if hash is null but verified is true 45 | * @since 1.3.0 46 | */ 47 | public HashUpdate(Hash hash) 48 | { 49 | this.hash = hash; 50 | } 51 | 52 | /** 53 | * @param hash the new hash 54 | * @param updated flag for updated hash 55 | * @throws BadParametersException if hash is null but verified is true 56 | * @since 1.7.0 57 | */ 58 | public HashUpdate(Hash hash, boolean updated) 59 | { 60 | this(hash); 61 | this.updated = updated; 62 | } 63 | 64 | /** 65 | * Returns the hash generated after a verification + update 66 | * process. 67 | *

68 | * It is never null if the hash is {@link #isVerified()} 69 | * return true. 70 | * 71 | * @return the regenerated hash 72 | * @since 1.3.0 73 | */ 74 | public Hash getHash() 75 | { 76 | return this.hash; 77 | } 78 | 79 | /** 80 | * Returns the result of the verification process. 81 | * 82 | * @return true if the verification process was successful 83 | * @since 1.3.0 84 | */ 85 | public boolean isVerified() 86 | { 87 | return hash != null; 88 | } 89 | 90 | /** 91 | * True if the update process changed the original hash due to changes on parameters. 92 | * Changing the algorithms always set this flag to true. 93 | * 94 | * @return true if the hash was updated 95 | * @since 1.7.0 96 | */ 97 | public boolean isUpdated() 98 | { 99 | return updated; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/PepperGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.password4j; 19 | 20 | 21 | /** 22 | * This class contains static functions that 23 | * help to create a secure pepper. 24 | *

25 | * In cryptography, a pepper is a secret added to a password 26 | * prior to being hashed with a CHF. 27 | * 28 | * @author David Bertoldi 29 | * @since 0.1.1 30 | */ 31 | public class PepperGenerator 32 | { 33 | 34 | private PepperGenerator() 35 | { 36 | // 37 | } 38 | 39 | /** 40 | * Generates a {@link String} that can be used as pepper 41 | * by a CHF. 42 | * The generated pepper is created by a cryptographically 43 | * strong random number generator (RNG). 44 | *

45 | * The parameter length must be a non-negative number, 46 | * otherwise a {@link BadParametersException} is thrown. 47 | * 48 | * @param length of the returned string 49 | * @return a pepper of the given length 50 | * @throws BadParametersException if the length is negative 51 | * @since 0.1.1 52 | */ 53 | public static String generate(int length) 54 | { 55 | if (length < 0) 56 | { 57 | throw new BadParametersException("Pepper length cannot be negative"); 58 | } 59 | return Utils.randomPrintable(length); 60 | } 61 | 62 | /** 63 | * Generates a {@link String} that can be used as pepper 64 | * by a CHF. 65 | * The generated pepper is created by a cryptographically 66 | * strong random number generator (RNG). 67 | *

68 | * The pepper generated is 24 characters long. 69 | * 70 | * @return a pepper as {@link String} 71 | * @since 0.1.1 72 | */ 73 | public static String generate() 74 | { 75 | return generate(24); 76 | } 77 | 78 | /** 79 | * Peppers by definition are shared between all passwords and 80 | * must be stored in a location different from the one used 81 | * for the passwords. 82 | *

83 | * It can be set in the psw4j.properties file with 84 | * the property {@code global.pepper}. 85 | *

86 | * If the psw4j.properties or the property {@code global.pepper} 87 | * are not found, {@code null} is returned. 88 | * 89 | * @return a shared pepper set in the psw4j.properties file. 90 | * @since 0.1.1 91 | */ 92 | public static String get() 93 | { 94 | return PropertyReader.readString("global.pepper", null, "Global pepper is not defined"); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/SaltGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.password4j; 19 | 20 | import java.security.SecureRandom; 21 | 22 | 23 | /** 24 | * This class contains static functions that 25 | * help to create a secure salt. 26 | *

27 | * In cryptography, a salt is random data that is used as 28 | * an additional input to a CHF. 29 | * 30 | * @author David Bertoldi 31 | * @since 0.1.0 32 | */ 33 | public class SaltGenerator 34 | { 35 | 36 | private SaltGenerator() 37 | { 38 | // 39 | } 40 | 41 | /** 42 | * Generates an array of {@code byte}s that can be used 43 | * as salt by the CHFs. 44 | * The generated salt is created by a cryptographically 45 | * strong random number generator (RNG). 46 | *

47 | * The parameter length must be a non-negative number, 48 | * otherwise a {@link BadParametersException} is thrown. 49 | * 50 | * @param length of the returned byte array 51 | * @return a salt as array of {@code byte}s 52 | * @throws BadParametersException if the length is negative 53 | * @since 0.1.0 54 | */ 55 | public static byte[] generate(int length) 56 | { 57 | if (length < 0) 58 | { 59 | throw new BadParametersException("Salt length cannot be negative"); 60 | } 61 | byte[] salt = new byte[length]; 62 | SecureRandom sr = AlgorithmFinder.getSecureRandom(); 63 | sr.nextBytes(salt); 64 | return salt; 65 | } 66 | 67 | /** 68 | * Generates an array of {@code byte}s that can be used 69 | * as salt by the CHFs. 70 | * The generated salt is created by a cryptographically 71 | * strong random number generator (RNG). 72 | *

73 | * The length of the array is 64. 74 | * 75 | * @return a salt as array of {@code byte}s 76 | * @since 0.1.0 77 | */ 78 | public static byte[] generate() 79 | { 80 | return generate(get()); 81 | } 82 | 83 | /** 84 | * Get the length of salt from configurations and 85 | * must be stored in a location different from the one used 86 | * for the passwords. 87 | *

88 | * It can be set in the psw4j.properties file with 89 | * the property {@code global.salt.length}. 90 | *

91 | * If the psw4j.properties or the property {@code global.salt.length} 92 | * are not found, {@code null} is returned. 93 | * 94 | * @return a shared pepper set in the psw4j.properties file. 95 | * @since 1.7.0 96 | */ 97 | public static int get() 98 | { 99 | return PropertyReader.readInt("global.salt.length", 64, "Global salt length is not defined in properties file"); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at dav.bertoldi at gmail.com . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/test/com/password4j/HashTest.java: -------------------------------------------------------------------------------- 1 | package com.password4j; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | import java.util.Arrays; 7 | 8 | 9 | public class HashTest 10 | { 11 | 12 | 13 | 14 | @Test 15 | public void testHashContent() 16 | { 17 | // GIVEN 18 | String hashed = "myHash"; 19 | String salt = "mySalt"; 20 | String pepper = "myPepper"; 21 | HashingFunction function = new CompressedPBKDF2Function(); 22 | 23 | // WHEN 24 | Hash hash = new Hash(function, hashed, hashed.getBytes(), salt); 25 | hash.setPepper(pepper); 26 | Hash hash2 = new Hash(function, hashed, hashed.getBytes(), salt); 27 | 28 | // THEN 29 | Assert.assertEquals(hashed, hash.getResult()); 30 | Assert.assertEquals(salt, hash.getSalt()); 31 | Assert.assertEquals(pepper, hash.getPepper()); 32 | Assert.assertEquals(Arrays.toString(hashed.getBytes()), Arrays.toString(hash.getBytes())); 33 | Assert.assertNull(hash2.getPepper()); 34 | } 35 | 36 | @Test 37 | public void testHashCheckNull() 38 | { 39 | // GIVEN 40 | Hash hash = Password.hash("myPassword").withCompressedPBKDF2(); 41 | 42 | // WHEN 43 | boolean result = Password.check((byte[]) null, hash); 44 | 45 | // THEN 46 | Assert.assertFalse(result); 47 | } 48 | 49 | @Test 50 | public void testHashEquality() 51 | { 52 | // GIVEN 53 | Hash hash = Password.hash("myPassword").withCompressedPBKDF2(); 54 | 55 | // WHEN 56 | boolean eq1 = hash.equals(null); 57 | boolean eq2 = hash.equals(new Object()); 58 | boolean eq3 = hash.equals(new Hash(AlgorithmFinder.getCompressedPBKDF2Instance(), hash.getResult(), hash.getBytes(), hash.getSaltBytes())); 59 | boolean eq4 = hash.equals(new Hash(AlgorithmFinder.getPBKDF2Instance(), hash.getResult(), hash.getBytes(), hash.getSalt())); 60 | boolean eq5 = hash.equals(new Hash(AlgorithmFinder.getCompressedPBKDF2Instance(), "hash", hash.getBytes(), hash.getSalt())); 61 | boolean eq6 = hash.equals(new Hash(AlgorithmFinder.getCompressedPBKDF2Instance(), hash.getResult(), hash.getBytes(), "salt")); 62 | boolean eq7 = hash.equals(new Hash(AlgorithmFinder.getCompressedPBKDF2Instance(), hash.getResult(), new byte[]{1,2,3,4}, hash.getSalt())); 63 | 64 | hash.setPepper("pepper"); 65 | Hash testingHash = new Hash(AlgorithmFinder.getCompressedPBKDF2Instance(), hash.getResult(), hash.getBytes(), hash.getSaltBytes()); 66 | 67 | testingHash.setPepper(hash.getPepper()); 68 | boolean eq8 = hash.equals(testingHash); 69 | hash.setPepper("reppep"); 70 | boolean eq9 = hash.equals(testingHash); 71 | 72 | // THEN 73 | Assert.assertFalse(eq1); 74 | Assert.assertFalse(eq2); 75 | Assert.assertTrue(eq3); 76 | Assert.assertFalse(eq4); 77 | Assert.assertFalse(eq5); 78 | Assert.assertFalse(eq6); 79 | Assert.assertFalse(eq7); 80 | Assert.assertTrue(eq8); 81 | Assert.assertFalse(eq9); 82 | } 83 | 84 | @Test 85 | public void testSecFunc() 86 | { 87 | // GIVEN 88 | Hash hash1 = Password.hash("myPassword").withCompressedPBKDF2(); 89 | Hash hash2 = Password.hash("myPassword").withPBKDF2(); 90 | Hash hash3 = Password.hash("myPassword").addPepper().withPBKDF2(); 91 | 92 | // WHEN 93 | String toString1 = hash1.toString(); 94 | int hc1 = hash1.hashCode(); 95 | String toString2 = hash2.toString(); 96 | int hc2 = hash2.hashCode(); 97 | String toString3 = hash3.toString(); 98 | Hash hash4 = new Hash(hash3.getHashingFunction(), hash3.getResult(), hash3.getBytes(), hash3.getSalt()); 99 | 100 | 101 | // THEN 102 | Assert.assertNotNull(toString1); 103 | Assert.assertNotNull(toString2); 104 | Assert.assertNotEquals(toString1, toString2); 105 | Assert.assertNotEquals(toString3, toString2); 106 | Assert.assertNotEquals(hc1, hc2); 107 | Assert.assertNotEquals(hash4, hash3); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /examples/MigrateFromMD5.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2022 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import com.password4j.BadParametersException; 19 | import com.password4j.Hash; 20 | import com.password4j.HashUpdate; 21 | import com.password4j.MessageDigestFunction; 22 | import com.password4j.Password; 23 | import com.password4j.ScryptFunction; 24 | 25 | 26 | /** 27 | * In this case the application used MD5 to hash passwords. 28 | * Here you can see how this dummy DAO verifies the credentials and updates the hash with a stronger algorithm, like scrypt. 29 | * Note that it already hashes the passwords with scrypt, so it takes in account not only old MD5 hashes but also the new ones with scrypt 30 | */ 31 | public class MigrateFromMD5 32 | { 33 | 34 | public class UserDAO 35 | { 36 | /** 37 | * Hash the password. 38 | * @param providedPassword password received from the user 39 | */ 40 | public void storePassword(String providedPassword) 41 | { 42 | Hash hash = Password.hash(providedPassword).withScrypt(); 43 | Service.storePasswordInDatabase(hash.getResult()); 44 | } 45 | 46 | /** 47 | * Verify if the password matches the one saved in the database 48 | * @param providedPassword password received from the user 49 | * @return true if mathces, false otherwise 50 | */ 51 | public boolean verifyCredentials(String providedPassword) 52 | { 53 | // Retrieve the password from the Database 54 | String passwordFromDB = Service.getPasswordFromDatabaseForCurrentUser(); 55 | 56 | try 57 | { 58 | // Parse the hash and tries to generate a scrypt prototype to use during the verification. 59 | // If the hash retrieved from the database is not a valid scrypt, a BadParametersException is thrown 60 | // and that means we found an old MD5 hash. 61 | ScryptFunction scrypt = ScryptFunction.getInstanceFromHash(passwordFromDB); 62 | 63 | if(Password.check(providedPassword, passwordFromDB).with(scrypt)) 64 | { 65 | // The check passed and the user is authenticated 66 | return true; 67 | } 68 | return false; 69 | } 70 | catch (BadParametersException bpe) 71 | { 72 | // If scrypt is not recognized it means we are working with a legacy MD5 hash! 73 | return handleOldPasswords(providedPassword, passwordFromDB); 74 | } 75 | } 76 | 77 | /** 78 | * If mathces, updates the hash with scrypt 79 | * @param providedPassword password received from the user 80 | * @param passwordFromDB password saved in the database 81 | * @return @return true if mathces, false otherwise 82 | */ 83 | private boolean handleOldPasswords(String providedPassword, String passwordFromDB) 84 | { 85 | // Create a MD5 prototype 86 | MessageDigestFunction md5 = MessageDigestFunction.getInstance("MD5"); 87 | 88 | // Check with MD5 and if the check passed it generates a stronger hash with scrypt 89 | HashUpdate update = Password.check(providedPassword, passwordFromDB).andUpdate().withScrypt(md5); 90 | 91 | // The check passed and the user can be authenticated 92 | if(update.isVerified()) 93 | { 94 | // Remember to store the new scrypt hash in the database 95 | Service.storePasswordInDatabase(update.getHash().getResult()); 96 | return true; 97 | } 98 | return false; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/test/com/password4j/SaltGeneratorTest.java: -------------------------------------------------------------------------------- 1 | package com.password4j; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import java.security.*; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | import static org.junit.Assert.assertNotEquals; 11 | 12 | 13 | public class SaltGeneratorTest 14 | { 15 | 16 | @Before 17 | public void init() 18 | { 19 | PropertyReader.properties.setProperty("global.random.strong", "false"); 20 | AlgorithmFinder.initialize(); 21 | } 22 | 23 | @Test 24 | public void testSaltLength() 25 | { 26 | // GIVEN 27 | int length = 23; 28 | 29 | // WHEN 30 | byte[] salt = SaltGenerator.generate(length); 31 | 32 | // THEN 33 | Assert.assertNotNull(salt); 34 | Assert.assertEquals(length, salt.length); 35 | } 36 | 37 | 38 | @Test 39 | public void testSaltNoLength() 40 | { 41 | // GIVEN 42 | 43 | // WHEN 44 | byte[] salt = SaltGenerator.generate(); 45 | 46 | // THEN 47 | Assert.assertNotNull(salt); 48 | Assert.assertEquals(16, salt.length); 49 | } 50 | 51 | @Test 52 | public void testSaltNoProp() 53 | { 54 | // GIVEN 55 | PropertyReader.properties.remove("global.salt.length"); 56 | 57 | // WHEN 58 | byte[] salt = SaltGenerator.generate(); 59 | 60 | // THEN 61 | Assert.assertNotNull(salt); 62 | Assert.assertEquals(64, salt.length); 63 | 64 | PropertyReader.properties.put("global.salt.length", "16"); 65 | } 66 | 67 | @Test(expected = BadParametersException.class) 68 | public void testSaltNegativeLength() 69 | { 70 | // GIVEN 71 | 72 | // WHEN 73 | SaltGenerator.generate(-3); 74 | 75 | // THEN 76 | 77 | } 78 | 79 | @Test 80 | public void testSaltZeroLength() 81 | { 82 | // GIVEN 83 | int length = 0; 84 | 85 | // WHEN 86 | byte[] salt = SaltGenerator.generate(length); 87 | 88 | // THEN 89 | Assert.assertNotNull(salt); 90 | Assert.assertEquals(length, salt.length); 91 | } 92 | 93 | @Test 94 | public void testStrongRandom() 95 | { 96 | // GIVEN 97 | 98 | PropertyReader.properties.setProperty("global.random.strong", "true"); 99 | 100 | // WHEN 101 | AlgorithmFinder.initialize(); 102 | 103 | // THEN 104 | 105 | try 106 | { 107 | assertEquals(SecureRandom.getInstanceStrong().getAlgorithm(), AlgorithmFinder.getSecureRandom().getAlgorithm()); 108 | 109 | } 110 | catch (NoSuchAlgorithmException e) 111 | { 112 | // 113 | } 114 | 115 | } 116 | 117 | @Test 118 | public void testStrongRandom2() 119 | { 120 | // GIVEN 121 | PropertyReader.properties.setProperty("global.random.strong", "true"); 122 | 123 | // WHEN 124 | 125 | String old = getSecurityProperty(); 126 | String strongAlg; 127 | try 128 | { 129 | strongAlg = SecureRandom.getInstanceStrong().getAlgorithm(); 130 | } 131 | catch (Exception e) 132 | { 133 | return; 134 | } 135 | 136 | setSecurityProperty("not and algorithm"); 137 | AlgorithmFinder.initialize(); 138 | 139 | // THEN 140 | 141 | 142 | assertNotEquals(strongAlg, AlgorithmFinder.getSecureRandom().getAlgorithm()); 143 | setSecurityProperty(old); 144 | 145 | } 146 | @SuppressWarnings("removal") 147 | private String getSecurityProperty() 148 | { 149 | return AccessController.doPrivileged((PrivilegedAction) () -> Security.getProperty("securerandom.strongAlgorithms")); 150 | } 151 | 152 | @SuppressWarnings("removal") 153 | private void setSecurityProperty(String value) 154 | { 155 | AccessController.doPrivileged((PrivilegedAction) () -> { 156 | Security.setProperty("securerandom.strongAlgorithms", value); 157 | return null; 158 | }); 159 | } 160 | 161 | @Test 162 | public void testSaltLength2() 163 | { 164 | //GIVEN 165 | 166 | //WHEN 167 | int saltLength = SaltGenerator.get(); 168 | 169 | //THEN 170 | Assert.assertEquals(16, saltLength); 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /src/test/com/password4j/Blake2bTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package com.password4j; 18 | 19 | import org.junit.Test; 20 | 21 | import java.util.ArrayList; 22 | import java.util.Arrays; 23 | import java.util.List; 24 | import java.util.concurrent.*; 25 | 26 | import static org.junit.Assert.assertEquals; 27 | import static org.junit.Assert.assertTrue; 28 | 29 | 30 | public class Blake2bTest 31 | { 32 | 33 | static class TestCase 34 | { 35 | String message; 36 | int length; 37 | String expected; 38 | 39 | public TestCase(String message, int length, String expected) 40 | { 41 | this.message = message; 42 | this.length = length; 43 | this.expected = expected; 44 | } 45 | } 46 | 47 | private static final List CASES = Arrays.asList( 48 | new TestCase("IamUsingBlake2b###", 512/8, "5fc5a199294099e98280dac6047523aa123ba29e6995618339c9590e4dca983dea2529ad85afbac5613c495b3fb50bf2d5919cb3f51f6a9dba78a33f9d278f6f"), 49 | new TestCase(null, 512/8, "786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce"), 50 | new TestCase(null, 384/8, "b32811423377f52d7862286ee1a72ee540524380fda1724a6f25d7978c6fd3244a6caf0498812673c5e05ef583825100"), 51 | new TestCase(null, 256/8, "0e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a8"), 52 | new TestCase(null, 224/8, "836cc68931c2e4e3e838602eca1902591d216837bafddfe6f0c8cb07"), 53 | 54 | new TestCase("0", 384/8, "c62e79958b2e7796d4b6afaba57b3a929a5c38125f56703cae90a952a96a6ef2a2d42376fe7183222779e3790fc95a22"), 55 | 56 | new TestCase("!$%^&*()_+@~{}", 512/8, "a7128f0b9a745d7073be967e2dc4ceb5e326a998ca45c451835c1c4eecd499dea1c1e04e15e890b2ac32675baea270785dd12d591646bc4df7c545b31041ed22") 57 | ); 58 | 59 | 60 | @Test 61 | public void test() 62 | { 63 | for (TestCase test : CASES) 64 | { 65 | Blake2b instance = new Blake2b(test.length); 66 | instance.update(test.message == null ? null : test.message.getBytes(Utils.DEFAULT_CHARSET)); 67 | byte[] out = new byte[test.length]; 68 | instance.doFinal(out, 0); 69 | assertEquals(test.expected, Utils.toHex(out)); 70 | } 71 | } 72 | 73 | 74 | @Test 75 | public void parallelTest() throws InterruptedException, ExecutionException 76 | { 77 | 78 | ExecutorService executors = Executors.newCachedThreadPool(); 79 | List> tasks = new ArrayList<>(); 80 | for (final TestCase test : CASES) 81 | { 82 | Callable c = () -> { 83 | Blake2b instance = new Blake2b(test.length); 84 | instance.update(test.message == null ? null : test.message.getBytes(Utils.DEFAULT_CHARSET)); 85 | byte[] out = new byte[test.length]; 86 | instance.doFinal(out, 0); 87 | return test.expected.equals(Utils.toHex(out)); 88 | }; 89 | tasks.add(c); 90 | } 91 | List> results = executors.invokeAll(tasks); 92 | 93 | for (Future future : results) 94 | { 95 | assertTrue(future.get()); 96 | } 97 | 98 | } 99 | 100 | @Test(expected = BadParametersException.class) 101 | public void badTest1() 102 | { 103 | // GIVEN 104 | 105 | // WHEN 106 | new Blake2b(0); 107 | } 108 | 109 | 110 | @Test(expected = BadParametersException.class) 111 | public void badTest2() 112 | { 113 | // GIVEN 114 | 115 | // WHEN 116 | new Blake2b(65); 117 | } 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | } 126 | 127 | -------------------------------------------------------------------------------- /src/test/com/password4j/PropertyReaderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package com.password4j; 18 | 19 | import org.junit.After; 20 | import org.junit.Assert; 21 | import org.junit.Before; 22 | import org.junit.Test; 23 | 24 | import java.io.File; 25 | import java.io.InputStream; 26 | 27 | 28 | public class PropertyReaderTest 29 | { 30 | 31 | @After 32 | @Before 33 | public void setup() 34 | { 35 | Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader()); 36 | System.clearProperty("psw4j.configuration"); 37 | PropertyReader.init(); 38 | } 39 | 40 | @Test 41 | public void testInt() 42 | { 43 | // GIVEN 44 | String key = "test.int"; 45 | 46 | // WHEN 47 | int ten = PropertyReader.readInt(key, -4, null); 48 | int minusNine = PropertyReader.readInt(key + "abc", -9, null); 49 | 50 | // THEN 51 | Assert.assertEquals(10, ten); 52 | Assert.assertEquals(-9, minusNine); 53 | } 54 | 55 | @Test 56 | public void testBool() 57 | { 58 | // GIVEN 59 | String key = "test.bool"; 60 | 61 | // WHEN 62 | boolean bool1 = PropertyReader.readBoolean(key, false); 63 | boolean bool2 = PropertyReader.readBoolean(key + "abc", true); 64 | 65 | // THEN 66 | Assert.assertTrue(bool1); 67 | Assert.assertTrue(bool2); 68 | } 69 | 70 | @Test 71 | public void testString() 72 | { 73 | // GIVEN 74 | String key = "test.string"; 75 | 76 | // WHEN 77 | String testString = PropertyReader.readString(key, "default string", null); 78 | String defaultValue = PropertyReader.readString(key + "abc", "default string", null); 79 | 80 | // THEN 81 | Assert.assertEquals("This is a string", testString); 82 | Assert.assertEquals("default string", defaultValue); 83 | } 84 | 85 | @Test 86 | public void testChar() 87 | { 88 | // GIVEN 89 | String key = "test.char"; 90 | 91 | // WHEN 92 | int backslash = PropertyReader.readChar(key, '/'); 93 | int slash = PropertyReader.readChar(key + "abc", '/'); 94 | int backslash2 = PropertyReader.readChar(key, '/', null); 95 | int slash2 = PropertyReader.readChar(key + "abc", '/', null); 96 | 97 | // THEN 98 | Assert.assertEquals('\\', backslash); 99 | Assert.assertEquals('/', slash); 100 | Assert.assertEquals('\\', backslash2); 101 | Assert.assertEquals('/', slash2); 102 | } 103 | 104 | @Test(expected = BadParametersException.class) 105 | public void testNull() 106 | { 107 | PropertyReader.readString(null, "null", null); 108 | } 109 | 110 | @Test 111 | public void testInitInvalidPath() 112 | { 113 | // GIVEN 114 | System.setProperty("psw4j.configuration", "/my/improbable/path/xyz.properties"); 115 | 116 | // WHEN 117 | PropertyReader.init(); 118 | 119 | // THEN 120 | Assert.assertTrue(PropertyReader.properties.isEmpty()); 121 | } 122 | 123 | @Test 124 | public void testInitCustomPath() throws Exception 125 | { 126 | // GIVEN 127 | String path = new File(".").getCanonicalPath() + "/src/test/my/custom/path/to/some.properties"; 128 | System.setProperty("psw4j.configuration", path); 129 | 130 | // WHEN 131 | PropertyReader.init(); 132 | 133 | // THEN 134 | Assert.assertFalse(PropertyReader.properties.isEmpty()); 135 | Assert.assertEquals("hello!!", PropertyReader.readString("check.this.out", "kappa", null)); 136 | } 137 | 138 | @Test 139 | public void testNoThreadClassLoader() throws Exception 140 | { 141 | // GIVEN 142 | String path = new File(".").getCanonicalPath() + "/src/test/my/custom/path/to/some.properties"; 143 | System.setProperty("psw4j.configuration", path); 144 | Thread.currentThread().setContextClassLoader(null); 145 | 146 | // WHEN 147 | PropertyReader.init(); 148 | 149 | // THEN 150 | Assert.assertFalse(PropertyReader.properties.isEmpty()); 151 | Assert.assertEquals("hello!!", PropertyReader.readString("check.this.out", "kappa", null)); 152 | } 153 | 154 | @Test 155 | public void testResource1() 156 | { 157 | // GIVEN 158 | 159 | // WHEN 160 | InputStream in = PropertyReader.getResource("PropertyReader.class"); 161 | // THEN 162 | Assert.assertNotNull(in); 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/SecureString.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.password4j; 19 | 20 | import java.util.Arrays; 21 | 22 | 23 | /** 24 | * More secure implementation of {@link CharSequence} than {@link String}, preventing heap memory attacks. 25 | *

26 | * Like {@link String}, this {@link CharSequence} implementation has an underlying array of {@code char}s. 27 | * The sequence however is not stored in the String pool and the array of {@code char}s lives 28 | * temporary in the heap memory. 29 | * 30 | * @author David Bertoldi 31 | * @since 1.2.0 32 | */ 33 | public class SecureString implements CharSequence 34 | { 35 | private final char[] chars; 36 | 37 | /** 38 | * Creates a {@link SecureString} from an array of {@code char}s. 39 | * The sequence is never put in the String pool. 40 | * 41 | * @param chars sequence of characters 42 | * @throws NullPointerException if null is passed 43 | * @since 1.2.0 44 | */ 45 | public SecureString(char[] chars) 46 | { 47 | this.chars = new char[chars.length]; 48 | System.arraycopy(chars, 0, this.chars, 0, chars.length); 49 | } 50 | 51 | /** 52 | * Creates a {@link SecureString} from an array of {@code char}s. 53 | * Important: if the second argument is true, the original array is zeroed after the object creation! Each {@code char} is replaced 54 | * with {@link Character#MIN_VALUE} 55 | *

56 | * The sequence is never put in the String pool. 57 | * 58 | * @param chars sequence of characters 59 | * @param eraseSource if true, the original array is zeroed 60 | * @throws NullPointerException if null is passed 61 | * @since 1.2.0 62 | */ 63 | public SecureString(char[] chars, boolean eraseSource) 64 | { 65 | this(chars); 66 | if (eraseSource) 67 | { 68 | clear(chars); 69 | } 70 | } 71 | 72 | /** 73 | * Creates a {@link SecureString} from a subsequence of an array of {@code char}s. 74 | * The sequence is never put in the String pool. 75 | * 76 | * @param chars sequence of characters 77 | * @param start index of the beginning of the subsequence 78 | * @param end index of the end of the subsequence 79 | * @throws NullPointerException if null is passed as array of {@code char}s 80 | * @since 1.2.0 81 | */ 82 | public SecureString(char[] chars, int start, int end) 83 | { 84 | this.chars = new char[end - start]; 85 | System.arraycopy(chars, start, this.chars, 0, this.chars.length); 86 | clear(chars); 87 | } 88 | 89 | private static synchronized void clear(char[] chars) 90 | { 91 | Arrays.fill(chars, Character.MIN_VALUE); 92 | } 93 | 94 | /** 95 | * @return length of the underlying array of {@code char}s. 96 | * @since 1.2.0 97 | */ 98 | @Override 99 | public synchronized int length() 100 | { 101 | return chars.length; 102 | } 103 | 104 | /** 105 | * @param index position in the underlying array of {@code char}s. 106 | * @return the {@code char} in the given position 107 | * @since 1.2.0 108 | */ 109 | @Override 110 | public synchronized char charAt(int index) 111 | { 112 | return chars[index]; 113 | } 114 | 115 | /** 116 | * Creates a {@link SecureString} from a subsequence of this object. 117 | * 118 | * @param start index of the beginning of the subsequence 119 | * @param end index of the end of the subsequence 120 | * @see SecureString#SecureString(char[], int, int) 121 | * @since 1.2.0 122 | */ 123 | @Override 124 | public synchronized CharSequence subSequence(int start, int end) 125 | { 126 | return new SecureString(this.chars, start, end); 127 | } 128 | 129 | /** 130 | * Manually clear the underlying array holding the characters 131 | * 132 | * @since 1.2.0 133 | */ 134 | public void clear() 135 | { 136 | synchronized (chars) 137 | { 138 | clear(chars); 139 | } 140 | } 141 | 142 | /** 143 | * Returns a constant {@link String} in order to prevent data leaks due 144 | * to accidental usage of a {@link SecureString} objects in methods like 145 | * {@link java.io.PrintStream#print(Object)}, loggers, etc. 146 | * 147 | * @return a masked version of this object. 148 | * @since 1.2.0 149 | */ 150 | @Override 151 | public String toString() 152 | { 153 | return "SecureString[****]"; 154 | } 155 | 156 | /** 157 | * Constant time equality to avoid potential timing attacks. 158 | * 159 | * @param other object 160 | * @since 1.2.1 161 | */ 162 | @Override 163 | public synchronized boolean equals(Object other) 164 | { 165 | if (this == other) 166 | { 167 | return true; 168 | } 169 | if (!(other instanceof CharSequence)) 170 | { 171 | return false; 172 | } 173 | CharSequence that = (CharSequence) other; 174 | 175 | if (chars.length != that.length()) 176 | { 177 | return false; 178 | } 179 | 180 | int equals = 0; 181 | for (int i = 0; i < chars.length; i++) 182 | { 183 | equals |= chars[i] ^ that.charAt(i); 184 | } 185 | 186 | return equals == 0; 187 | } 188 | 189 | @Override 190 | public synchronized int hashCode() 191 | { 192 | return Arrays.hashCode(chars); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/AbstractHashingFunction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.password4j; 19 | 20 | /** 21 | * Class in the hierarchy to avoid code duplication. 22 | * 23 | * @author David Bertoldi 24 | * @since 0.1.0 25 | */ 26 | public abstract class AbstractHashingFunction implements HashingFunction 27 | { 28 | 29 | /** 30 | * Compares two {@link CharSequence}s as byte arrays in length-constant time. This comparison method 31 | * is used so that password hashes cannot be extracted from an on-line 32 | * system using a timing attack and then attacked off-line. 33 | * 34 | * @param a the first CharSequence 35 | * @param b the second CharSequence 36 | * @return true if both {@link CharSequence}s are the same, false if not 37 | */ 38 | protected static boolean slowEquals(CharSequence a, CharSequence b) 39 | { 40 | return slowEquals(Utils.fromCharSequenceToBytes(a), Utils.fromCharSequenceToBytes(b)); 41 | } 42 | 43 | /** 44 | * Compares two byte arrays in length-constant time. This comparison method 45 | * is used so that password hashes cannot be extracted from an on-line 46 | * system using a timing attack and then attacked off-line. 47 | * 48 | * @param a the first byte array 49 | * @param b the second byte array 50 | * @return true if both byte arrays are the same, false if not 51 | */ 52 | protected static boolean slowEquals(byte[] a, byte[] b) 53 | { 54 | int diff = a.length ^ b.length; 55 | for (int i = 0; i < a.length && i < b.length; i++) 56 | { 57 | diff |= a[i] ^ b[i]; 58 | } 59 | return diff == 0; 60 | } 61 | 62 | @Override 63 | public Hash hash(CharSequence plainTextPassword, String salt, CharSequence pepper) 64 | { 65 | CharSequence peppered = Utils.append(pepper, plainTextPassword); 66 | Hash result; 67 | if (salt == null) 68 | { 69 | result = hash(peppered); 70 | } 71 | else 72 | { 73 | result = hash(peppered, salt); 74 | } 75 | 76 | result.setPepper(pepper); 77 | return result; 78 | } 79 | 80 | @Override 81 | public Hash hash(byte[] plainTextPassword, byte[] salt, CharSequence pepper) 82 | { 83 | byte[] pepperAsBytes = Utils.fromCharSequenceToBytes(pepper); 84 | byte[] peppered = Utils.append(pepperAsBytes, plainTextPassword); 85 | Hash result; 86 | if (salt == null) 87 | { 88 | result = hash(peppered); 89 | } 90 | else 91 | { 92 | result = hash(peppered, salt); 93 | } 94 | 95 | result.setPepper(pepper); 96 | return result; 97 | } 98 | 99 | /** 100 | * Just calls {@link #check(CharSequence, String)} without salt 101 | * parameter. 102 | *

103 | * Do not override this if the algorithm doesn't need a manually 104 | * provided salt. 105 | * 106 | * @param plainTextPassword the plaintext password 107 | * @param hashed the hash 108 | * @param salt the salt used to produce the hash 109 | * @return true if the hash is generated from the plaintext; false otherwise 110 | * @since 0.1.0 111 | */ 112 | @Override 113 | public boolean check(CharSequence plainTextPassword, String hashed, String salt) 114 | { 115 | return check(plainTextPassword, hashed); 116 | } 117 | 118 | /** 119 | * Just calls {@link #check(CharSequence, String)} without salt 120 | * parameter. 121 | *

122 | * Do not override this if the algorithm doesn't need a manually 123 | * provided salt. 124 | * 125 | * @param plainTextPassword the plaintext password as bytes array 126 | * @param hashed the hash as bytes array 127 | * @param salt the salt as bytes array used to produce the hash 128 | * @return true if the hash is generated from the plaintext; false otherwise 129 | * @since 1.7.0 130 | */ 131 | @Override 132 | public boolean check(byte[] plainTextPassword, byte[] hashed, byte[] salt) 133 | { 134 | return check(plainTextPassword, hashed); 135 | } 136 | 137 | /** 138 | * Just calls {@link #check(CharSequence, String, String)}, with a prepended pepper. 139 | * 140 | * @param plainTextPassword the plaintext password 141 | * @param hashed the hash 142 | * @param salt the salt used to produce the hash 143 | * @return true if the hash is generated from the plaintext; false otherwise 144 | * @since 1.5.0 145 | */ 146 | @Override 147 | public boolean check(CharSequence plainTextPassword, String hashed, String salt, CharSequence pepper) 148 | { 149 | return check(Utils.append(pepper, plainTextPassword), hashed, salt); 150 | } 151 | 152 | /** 153 | * Just calls {@link #check(CharSequence, String, String)}, with a prepended pepper. 154 | * 155 | * @param plainTextPassword the plaintext password 156 | * @param hashed the hash 157 | * @param salt the salt used to produce the hash 158 | * @return true if the hash is generated from the plaintext; false otherwise 159 | * @since 1.7.0 160 | */ 161 | @Override 162 | public boolean check(byte[] plainTextPassword, byte[] hashed, byte[] salt, CharSequence pepper) 163 | { 164 | byte[] pepperAsBytes = Utils.fromCharSequenceToBytes(pepper); 165 | return check(Utils.append(pepperAsBytes, plainTextPassword), hashed, salt); 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /src/test/com/password4j/SystemCheckTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.password4j; 19 | 20 | import com.password4j.types.Argon2; 21 | import com.password4j.types.Hmac; 22 | import org.junit.Assert; 23 | import org.junit.Test; 24 | 25 | import java.util.Set; 26 | 27 | 28 | public class SystemCheckTest 29 | { 30 | 31 | @Test 32 | public void testPBKDF2Iterations() 33 | { 34 | // GIVEN 35 | long maxMilliseconds = 10; 36 | 37 | // WHEN 38 | BenchmarkResult result = SystemChecker.benchmarkPBKDF2(maxMilliseconds, Hmac.SHA256, 256); 39 | 40 | // THEN 41 | Assert.assertTrue(result.getPrototype().getIterations() > 150); 42 | Assert.assertTrue(result.getElapsed() <= maxMilliseconds); 43 | } 44 | 45 | @Test 46 | public void testPBKDF2Iterations2() 47 | { 48 | // GIVEN 49 | long maxMilliseconds = -1; 50 | 51 | // WHEN 52 | BenchmarkResult result = SystemChecker.benchmarkPBKDF2(maxMilliseconds, Hmac.SHA512, 4096); 53 | 54 | // THEN 55 | Assert.assertNull(result.getPrototype()); 56 | Assert.assertEquals(-1, result.getElapsed()); 57 | } 58 | 59 | 60 | @Test 61 | public void testArgon2Iterations() 62 | { 63 | // GIVEN 64 | long maxMilliseconds = 50; 65 | int memoryForEachHash = 512; 66 | int threadsPerHash = 2; 67 | int outputLength = 128; 68 | Argon2 type = Argon2.ID; 69 | 70 | // WHEN 71 | BenchmarkResult result = SystemChecker.benchmarkForArgon2(maxMilliseconds, memoryForEachHash, threadsPerHash, outputLength, type); 72 | 73 | // THEN 74 | Assert.assertTrue(result.getPrototype().getIterations() > 1); 75 | Assert.assertTrue(result.getElapsed() <= maxMilliseconds); 76 | } 77 | 78 | @Test 79 | public void testArgon2Iterations2() 80 | { 81 | // GIVEN 82 | long maxMilliseconds = 2; 83 | int memoryForEachHash = 4096; 84 | int threadsPerHash = 21; 85 | int outputLength = 128; 86 | Argon2 type = Argon2.ID; 87 | 88 | // WHEN 89 | BenchmarkResult result = SystemChecker.benchmarkForArgon2(maxMilliseconds, memoryForEachHash, threadsPerHash, outputLength, type); 90 | 91 | // THEN 92 | Assert.assertNull(result.getPrototype()); 93 | Assert.assertEquals(-1, result.getElapsed()); 94 | } 95 | 96 | 97 | @Test 98 | public void testBcryptRounds() 99 | { 100 | // GIVEN 101 | long maxMilliseconds = 50; 102 | 103 | // WHEN 104 | BenchmarkResult result = SystemChecker.benchmarkBcrypt(maxMilliseconds); 105 | 106 | // THEN 107 | Assert.assertTrue(result.getPrototype().getLogarithmicRounds() >= 4); 108 | Assert.assertTrue(result.getElapsed() <= maxMilliseconds); 109 | } 110 | 111 | @Test 112 | public void testBcryptRounds2() 113 | { 114 | // GIVEN 115 | long maxMilliseconds = -1; 116 | 117 | // WHEN 118 | BenchmarkResult result = SystemChecker.benchmarkBcrypt(maxMilliseconds); 119 | 120 | // THEN 121 | Assert.assertNull(result.getPrototype()); 122 | Assert.assertEquals(-1, result.getElapsed()); 123 | } 124 | 125 | @Test 126 | public void testScryptRounds() 127 | { 128 | // GIVEN 129 | long maxMilliseconds = -1; 130 | 131 | // WHEN 132 | BenchmarkResult result1 = SystemChecker.findWorkFactorForScrypt(maxMilliseconds, 16, 1); 133 | BenchmarkResult result2 = SystemChecker.findResourcesForScrypt(maxMilliseconds, 1024, 1); 134 | 135 | // THEN 136 | Assert.assertNull(result1.getPrototype()); 137 | Assert.assertEquals(-1, result1.getElapsed()); 138 | Assert.assertNull(result2.getPrototype()); 139 | Assert.assertEquals(-1, result2.getElapsed()); 140 | } 141 | 142 | @Test 143 | public void testScryptRounds2() 144 | { 145 | // GIVEN 146 | long maxMilliseconds = 50; 147 | 148 | // WHEN 149 | BenchmarkResult result1 = SystemChecker.findWorkFactorForScrypt(maxMilliseconds, 16, 1); 150 | BenchmarkResult result2 = SystemChecker.findResourcesForScrypt(maxMilliseconds, result1.getPrototype().getWorkFactor(), 1); 151 | 152 | // THEN 153 | Assert.assertTrue(result1.getElapsed() > 0); 154 | Assert.assertTrue(result2.getElapsed() > 0); 155 | } 156 | 157 | 158 | 159 | @Test(expected = BadParametersException.class) 160 | public void testWrongVariants() 161 | { 162 | //GIVEN 163 | 164 | // WHEN 165 | SystemChecker.isPBKDF2Supported(null); 166 | } 167 | 168 | @Test(expected = Test.None.class) 169 | public void testVariants() 170 | { 171 | //GIVEN 172 | 173 | // WHEN 174 | SystemChecker.isPBKDF2Supported(Hmac.SHA256.name()); 175 | } 176 | 177 | 178 | @Test(expected = BadParametersException.class) 179 | public void testWrongAlgs() 180 | { 181 | //GIVEN 182 | 183 | // WHEN 184 | SystemChecker.isMessageDigestSupported(null); 185 | } 186 | 187 | @Test 188 | public void testAlgs() 189 | { 190 | //GIVEN 191 | 192 | // WHEN 193 | Set mds = AlgorithmFinder.getAllMessageDigests(); 194 | for(String md : mds) 195 | { 196 | Assert.assertTrue(SystemChecker.isMessageDigestSupported(md)); 197 | } 198 | 199 | } 200 | 201 | 202 | 203 | 204 | } 205 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/PropertyReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package com.password4j; 18 | 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | import java.io.FileInputStream; 23 | import java.io.FileNotFoundException; 24 | import java.io.IOException; 25 | import java.io.InputStream; 26 | import java.security.AccessControlException; 27 | import java.util.Properties; 28 | 29 | 30 | class PropertyReader 31 | { 32 | 33 | private static final Logger LOG = LoggerFactory.getLogger(PropertyReader.class); 34 | 35 | private static final String FILE_NAME = "psw4j.properties"; 36 | 37 | private static final String CONFIGURATION_KEY = "psw4j.configuration"; 38 | 39 | private static final String MESSAGE = "{}. Default value is used ({}). Please set property {} in your " + FILE_NAME + " file."; 40 | 41 | protected static Properties properties; 42 | 43 | static 44 | { 45 | init(); 46 | } 47 | 48 | private PropertyReader() 49 | { 50 | // 51 | } 52 | 53 | static int readInt(String key, int defaultValue, String message) 54 | { 55 | String str = readString(key); 56 | if (str == null) 57 | { 58 | LOG.warn(MESSAGE, message, defaultValue, key); 59 | return defaultValue; 60 | } 61 | return Integer.parseInt(str); 62 | } 63 | 64 | static boolean readBoolean(String key, boolean defaultValue) 65 | { 66 | String str = readString(key); 67 | if (str == null) 68 | { 69 | return defaultValue; 70 | } 71 | return Boolean.parseBoolean(str); 72 | } 73 | 74 | static String readString(String key, String defaultValue, String message) 75 | { 76 | String value = readString(key); 77 | if (value == null) 78 | { 79 | LOG.warn(MESSAGE, message, defaultValue, key); 80 | return defaultValue; 81 | } 82 | return value; 83 | } 84 | 85 | static char readChar(String key, char defaultValue, String message) 86 | { 87 | String str = readString(key); 88 | if (str == null) 89 | { 90 | LOG.warn(MESSAGE, message, defaultValue, key); 91 | return defaultValue; 92 | } 93 | return str.charAt(0); 94 | } 95 | 96 | static char readChar(String key, char defaultValue) 97 | { 98 | String str = readString(key); 99 | if (str == null) 100 | { 101 | return defaultValue; 102 | } 103 | return str.charAt(0); 104 | } 105 | 106 | private static String readString(String key) 107 | { 108 | if (key == null) 109 | { 110 | throw new BadParametersException("Key cannot be null"); 111 | } 112 | 113 | if (properties != null) 114 | { 115 | return properties.getProperty(key); 116 | } 117 | return null; 118 | } 119 | 120 | static void init() 121 | { 122 | String customPath = null; 123 | 124 | try 125 | { 126 | customPath = System.getProperty(CONFIGURATION_KEY, null); 127 | } 128 | catch (AccessControlException ex) 129 | { 130 | LOG.debug("Cannot access configuration key property", ex); 131 | } 132 | 133 | InputStream in = null; 134 | Properties props = new Properties(); 135 | try 136 | { 137 | if (customPath == null || customPath.isEmpty()) 138 | { 139 | in = getResource('/' + FILE_NAME); 140 | } 141 | else 142 | { 143 | in = getResource(customPath); 144 | } 145 | } 146 | catch (AccessControlException ex) 147 | { 148 | LOG.debug("Cannot access properties file", ex); 149 | props.setProperty("global.banner", "false"); 150 | } 151 | 152 | if (in != null) 153 | { 154 | try 155 | { 156 | props.load(in); 157 | } 158 | catch (IOException e) 159 | { 160 | // 161 | } 162 | } 163 | else 164 | { 165 | LOG.debug("Cannot find any properties file."); 166 | } 167 | 168 | properties = props; 169 | } 170 | 171 | static InputStream getResource(String resource) 172 | { 173 | ClassLoader classLoader; 174 | InputStream in; 175 | 176 | try 177 | { 178 | classLoader = Thread.currentThread().getContextClassLoader(); 179 | if (classLoader != null) 180 | { 181 | in = classLoader.getResourceAsStream(resource); 182 | if (in != null) 183 | { 184 | return in; 185 | } 186 | } 187 | 188 | // Try with the class loader that loaded this class 189 | classLoader = PropertyReader.class.getClassLoader(); 190 | if (classLoader != null) 191 | { 192 | in = classLoader.getResourceAsStream(resource); 193 | if (in != null) 194 | { 195 | return in; 196 | } 197 | } 198 | } 199 | catch (Exception e) 200 | { 201 | LOG.warn("", e); 202 | } 203 | 204 | // Get the resource from the class path in case that the class is loaded 205 | // by the Extension class loader which the parent of the system class loader. 206 | in = ClassLoader.getSystemResourceAsStream(resource); 207 | if (in != null) 208 | { 209 | return in; 210 | } 211 | 212 | try 213 | { 214 | return new FileInputStream(resource); 215 | } 216 | catch (FileNotFoundException e) 217 | { 218 | return PropertyReader.class.getResourceAsStream(resource); 219 | } 220 | 221 | } 222 | 223 | } 224 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ## Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | We as members, contributors, and leaders pledge to make participation in our 24 | community a harassment-free experience for everyone, regardless of age, body 25 | size, visible or invisible disability, ethnicity, sex characteristics, gender 26 | identity and expression, level of experience, education, socio-economic status, 27 | nationality, personal appearance, race, religion, or sexual identity 28 | and orientation. 29 | 30 | We pledge to act and interact in ways that contribute to an open, welcoming, 31 | diverse, inclusive, and healthy community. 32 | 33 | ### Our Standards 34 | 35 | Examples of behavior that contributes to a positive environment for our 36 | community include: 37 | 38 | * Demonstrating empathy and kindness toward other people 39 | * Being respectful of differing opinions, viewpoints, and experiences 40 | * Giving and gracefully accepting constructive feedback 41 | * Accepting responsibility and apologizing to those affected by our mistakes, 42 | and learning from the experience 43 | * Focusing on what is best not just for us as individuals, but for the 44 | overall community 45 | 46 | Examples of unacceptable behavior include: 47 | 48 | * The use of sexualized language or imagery, and sexual attention or 49 | advances of any kind 50 | * Trolling, insulting or derogatory comments, and personal or political attacks 51 | * Public or private harassment 52 | * Publishing others' private information, such as a physical or email 53 | address, without their explicit permission 54 | * Other conduct which could reasonably be considered inappropriate in a 55 | professional setting 56 | 57 | ### Enforcement Responsibilities 58 | 59 | Community leaders are responsible for clarifying and enforcing our standards of 60 | acceptable behavior and will take appropriate and fair corrective action in 61 | response to any behavior that they deem inappropriate, threatening, offensive, 62 | or harmful. 63 | 64 | Community leaders have the right and responsibility to remove, edit, or reject 65 | comments, commits, code, wiki edits, issues, and other contributions that are 66 | not aligned to this Code of Conduct, and will communicate reasons for moderation 67 | decisions when appropriate. 68 | 69 | ### Scope 70 | 71 | This Code of Conduct applies within all community spaces, and also applies when 72 | an individual is officially representing the community in public spaces. 73 | Examples of representing our community include using an official e-mail address, 74 | posting via an official social media account, or acting as an appointed 75 | representative at an online or offline event. 76 | 77 | ### Enforcement 78 | 79 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 80 | reported to the community leaders. 81 | All complaints will be reviewed and investigated promptly and fairly. 82 | 83 | All community leaders are obligated to respect the privacy and security of the 84 | reporter of any incident. 85 | 86 | ### Enforcement Guidelines 87 | 88 | Community leaders will follow these Community Impact Guidelines in determining 89 | the consequences for any action they deem in violation of this Code of Conduct: 90 | 91 | #### 1. Correction 92 | 93 | **Community Impact**: Use of inappropriate language or other behavior deemed 94 | unprofessional or unwelcome in the community. 95 | 96 | **Consequence**: A private, written warning from community leaders, providing 97 | clarity around the nature of the violation and an explanation of why the 98 | behavior was inappropriate. A public apology may be requested. 99 | 100 | #### 2. Warning 101 | 102 | **Community Impact**: A violation through a single incident or series 103 | of actions. 104 | 105 | **Consequence**: A warning with consequences for continued behavior. No 106 | interaction with the people involved, including unsolicited interaction with 107 | those enforcing the Code of Conduct, for a specified period of time. This 108 | includes avoiding interactions in community spaces as well as external channels 109 | like social media. Violating these terms may lead to a temporary or 110 | permanent ban. 111 | 112 | #### 3. Temporary Ban 113 | 114 | **Community Impact**: A serious violation of community standards, including 115 | sustained inappropriate behavior. 116 | 117 | **Consequence**: A temporary ban from any sort of interaction or public 118 | communication with the community for a specified period of time. No public or 119 | private interaction with the people involved, including unsolicited interaction 120 | with those enforcing the Code of Conduct, is allowed during this period. 121 | Violating these terms may lead to a permanent ban. 122 | 123 | #### 4. Permanent Ban 124 | 125 | **Community Impact**: Demonstrating a pattern of violation of community 126 | standards, including sustained inappropriate behavior, harassment of an 127 | individual, or aggression toward or disparagement of classes of individuals. 128 | 129 | **Consequence**: A permanent ban from any sort of public interaction within 130 | the community. 131 | 132 | ### Attribution 133 | 134 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 135 | version 2.0, available at 136 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 137 | 138 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 139 | enforcement ladder](https://github.com/mozilla/diversity). 140 | 141 | [homepage]: https://www.contributor-covenant.org 142 | 143 | For answers to common questions about this code of conduct, see the FAQ at 144 | https://www.contributor-covenant.org/faq. Translations are available at 145 | https://www.contributor-covenant.org/translations. -------------------------------------------------------------------------------- /src/test/com/password4j/BalloonHashingFunctionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2023 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.password4j; 19 | 20 | import com.password4j.types.Argon2; 21 | import com.password4j.types.Bcrypt; 22 | import org.junit.Assert; 23 | import org.junit.Test; 24 | 25 | import java.nio.charset.StandardCharsets; 26 | import java.util.Arrays; 27 | 28 | public class BalloonHashingFunctionTest 29 | { 30 | 31 | private static final Object[][] TEST_VECTORS = new Object[][]{ 32 | // Single thread 33 | new Object[]{"hunter42", "examplesalt", "SHA-256", 1024, 3, 0, 3, "716043dff777b44aa7b88dcbab12c078abecfac9d289c5b5195967aa63440dfb"}, 34 | new Object[]{"", "salt", "SHA-256", 3, 3, 0, 3, "5f02f8206f9cd212485c6bdf85527b698956701ad0852106f94b94ee94577378"}, 35 | new Object[]{"password", "", "SHA-256", 3, 3, 0, 3, "20aa99d7fe3f4df4bd98c655c5480ec98b143107a331fd491deda885c4d6a6cc"}, 36 | new Object[]{"\0", "\0", "SHA-256", 3, 3, 0, 3, "4fc7e302ffa29ae0eac31166cee7a552d1d71135f4e0da66486fb68a749b73a4"}, 37 | new Object[]{"password", "salt", "SHA-256", 1, 1, 0, 3, "eefda4a8a75b461fa389c1dcfaf3e9dfacbc26f81f22e6f280d15cc18c417545"}, 38 | 39 | // Multiple threads 40 | new Object[]{"hunter42", "examplesalt", "SHA-256", 1024, 3, 4, 3, "1832bd8e5cbeba1cb174a13838095e7e66508e9bf04c40178990adbc8ba9eb6f"}, 41 | new Object[]{"", "salt", "SHA-256", 3, 3, 2, 3, "f8767fe04059cef67b4427cda99bf8bcdd983959dbd399a5e63ea04523716c23"}, 42 | new Object[]{"password", "", "SHA-256", 3, 3, 3, 3, "bcad257eff3d1090b50276514857e60db5d0ec484129013ef3c88f7d36e438d6"}, 43 | new Object[]{"password", "", "SHA-256", 3, 3, 1, 3, "498344ee9d31baf82cc93ebb3874fe0b76e164302c1cefa1b63a90a69afb9b4d"}, 44 | new Object[]{"\000", "\000", "SHA-256", 3, 3, 4, 3, "8a665611e40710ba1fd78c181549c750f17c12e423c11930ce997f04c7153e0c"}, 45 | new Object[]{"\000", "\000", "SHA-256", 3, 3, 1, 3, "d9e33c683451b21fb3720afbd78bf12518c1d4401fa39f054b052a145c968bb1"}, 46 | new Object[]{"password", "salt", "SHA-256", 1, 1, 16, 3, "a67b383bb88a282aef595d98697f90820adf64582a4b3627c76b7da3d8bae915"}, 47 | new Object[]{"password", "salt", "SHA-256", 1, 1, 1, 3, "97a11df9382a788c781929831d409d3599e0b67ab452ef834718114efdcd1c6d"}, 48 | 49 | }; 50 | 51 | 52 | @Test 53 | public void test() 54 | { 55 | 56 | BalloonHashingFunction balloonHashingFunction; 57 | for (Object[] testVector : TEST_VECTORS) 58 | { 59 | balloonHashingFunction = new BalloonHashingFunction((String) testVector[2], (Integer) testVector[3], (Integer) testVector[4], (Integer) testVector[5], (Integer) testVector[6]); 60 | Assert.assertEquals(testVector[7], balloonHashingFunction.hash((String) testVector[0], (String) testVector[1]).getResult()); 61 | 62 | Assert.assertTrue(balloonHashingFunction.check((String) testVector[0], (String) testVector[7], (String) testVector[1])); 63 | } 64 | 65 | } 66 | 67 | @Test 68 | public void testInstance() 69 | { 70 | 71 | BalloonHashingFunction balloonHashingFunction; 72 | for (Object[] testVector : TEST_VECTORS) 73 | { 74 | balloonHashingFunction = BalloonHashingFunction.getInstance((String) testVector[2], (Integer) testVector[3], (Integer) testVector[4], (Integer) testVector[5], (Integer) testVector[6]); 75 | Assert.assertEquals(testVector[7], balloonHashingFunction.hash((String) testVector[0], (String) testVector[1]).getResult()); 76 | Assert.assertEquals(testVector[7], balloonHashingFunction.hash(((String) testVector[0]).getBytes(), ((String) testVector[1]).getBytes()).getResult()); 77 | 78 | Assert.assertTrue(balloonHashingFunction.check((String) testVector[0], (String) testVector[7], (String) testVector[1])); 79 | Assert.assertTrue(balloonHashingFunction.check(((String) testVector[0]).getBytes(), ((String) testVector[7]).getBytes(), ((String) testVector[1]).getBytes())); 80 | } 81 | 82 | } 83 | 84 | @Test 85 | public void testEquality() 86 | { 87 | // GIVEN 88 | String m = "SHA-256"; 89 | int i = 2; 90 | int p = 3; 91 | int l = 4; 92 | int v = 5; 93 | BalloonHashingFunction balloonHashingFunction = BalloonHashingFunction.getInstance(m, i, p, l, v); 94 | 95 | // THEN 96 | boolean eqNull = balloonHashingFunction.equals(null); 97 | boolean eqClass = balloonHashingFunction.equals(new BcryptFunction(Bcrypt.A, 10)); 98 | boolean sameInst = balloonHashingFunction.equals(BalloonHashingFunction.getInstance(m, i, p, l, v)); 99 | boolean sameInst2 = balloonHashingFunction.equals(new BalloonHashingFunction(m, i, p, l, v)); 100 | String toString = balloonHashingFunction.toString(); 101 | int hashCode = balloonHashingFunction.hashCode(); 102 | boolean notSameInst1 = balloonHashingFunction.equals(new BalloonHashingFunction("SHA-512", i, p, l, v)); 103 | boolean notSameInst2 = balloonHashingFunction.equals(new BalloonHashingFunction(m, i+1, p, l, v)); 104 | boolean notSameInst3 = balloonHashingFunction.equals(new BalloonHashingFunction(m, i, p+1, l, v)); 105 | boolean notSameInst4 = balloonHashingFunction.equals(new BalloonHashingFunction(m, i, p, l+1, v)); 106 | boolean notSameInst6 = balloonHashingFunction.equals(new BalloonHashingFunction(m, i, p, l, v+1)); 107 | 108 | // END 109 | Assert.assertFalse(eqNull); 110 | Assert.assertFalse(eqClass); 111 | Assert.assertTrue(sameInst); 112 | Assert.assertTrue(sameInst2); 113 | Assert.assertNotEquals(toString, new BalloonHashingFunction(m, i+1, p, l, v).toString()); 114 | Assert.assertNotEquals(hashCode, new BalloonHashingFunction(m, i, p, l, v+1).hashCode()); 115 | Assert.assertFalse(notSameInst1); 116 | Assert.assertFalse(notSameInst2); 117 | Assert.assertFalse(notSameInst3); 118 | Assert.assertFalse(notSameInst4); 119 | Assert.assertFalse(notSameInst6); 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/test/org/example/project/PublicPasswordTest.java: -------------------------------------------------------------------------------- 1 | package org.example.project; 2 | 3 | import com.password4j.*; 4 | import com.password4j.types.Argon2; 5 | import com.password4j.types.Hmac; 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | 9 | 10 | public class PublicPasswordTest 11 | { 12 | 13 | private static final TestSuite[] PBKDF2_TEST = new TestSuite[]{ 14 | new TestSuite("r+bFUweFtsxrHGRTOEcxvV7kMu5Un9QvtmlXea2KHFv1neacSPd078QAfVKY+QM8AkHVq2kwXntk7O642DTP7A==", "password", 15 | "salt", null, PBKDF2Function.getInstance(Hmac.SHA512, 1000, 512)), 16 | 17 | new TestSuite("x/daChKTPQGTKZrBsmPIqJ3KtqcaYni8FqdziEgPRw9gowIpZxfzW7UI8gqZj0pI5xChr5RDxjYjc8yMbucHHw==", "password", 18 | "salt", "pepper", PBKDF2Function.getInstance(Hmac.SHA512, 1000, 512)), 19 | 20 | new TestSuite("EgvuM3qhGradmNwl2b1Z5uPnasY=", "123", "456", "", 21 | PBKDF2Function.getInstance(Hmac.SHA1, 49999, 160)), 22 | 23 | new TestSuite("$2a$06$If6bvum7DFjUnE9p2uDeDu0YHzrHM6tf.iqN8.yx.jNN1ILEf7h0i", "bc", "$2a$06$If6bvum7DFjUnE9p2uDeDu", "a", 24 | BcryptFunction.getInstance(6)), 25 | 26 | new TestSuite("$2a$09$PVRpK74XnUl/dsFfw6YSsOgwnJ1N3b5jKgbR/qdqerkuIYMa2u6eG", "Password", "$2a$09$PVRpK74XnUl/dsFfw6YSsO", "my", 27 | BcryptFunction.getInstance(9)), 28 | 29 | new TestSuite("$2a$07$W3mOfB5auMDG3EitumH0S.ffmkA.NZIOZFaXb15tPWQyqq0hDXiEC", "Alice", "$2a$07$W3mOfB5auMDG3EitumH0S.", null, 30 | BcryptFunction.getInstance(7)), 31 | 32 | new TestSuite("$2a$14$7rdjAp2vQxO0hCK9GvniqeKURflehmGaW5C2CLOONKZauODS7xOGW", "password4j", "$2a$14$7rdjAp2vQxO0hCK9Gvniqe", null, 33 | BcryptFunction.getInstance(14)), 34 | 35 | new TestSuite("$e0801$c2FsdA==$dFcxr0SE8yOWiWntoomu7gBbWQOsVh5kpayhIXl793NO+f1YQi4uIhg7ysup7Ie6DIO3oueI8Dzg2gZGNDPNpg==", "word", "salt", 36 | "pass", ScryptFunction.getInstance(16384, 8, 1)), 37 | 38 | new TestSuite("$a0402$bm90UmFuZG9t$upriFfo7v+aAUqOKDpguh0duZlAHiKcQOLM0k/xFcBg7qfRcDfYLEZe/60+b+4NtA1M70LUI0IRY+3+ybuLMZg==", "known", "notRandom", 39 | "un", ScryptFunction.getInstance(1024, 4, 2)), 40 | 41 | new TestSuite("$argon2id$v=19$m=1024,t=3,p=12$MTExMTExMTE$0PUE8wVEaK0qdjms3b4pTZOs0+00S/+9j28WZ3gMUno", "first!", "11111111", 42 | null, Argon2Function.getInstance(1024, 3, 12, 32, Argon2.ID)), 43 | 44 | new TestSuite("$argon2id$v=19$m=4096,t=20,p=4$NUdyNEV4Yzc3RG9QOQ$aY701D5E9/hCqO4HMVChlGuR2PPYntYLcr8RsxFi/Xo", "password", "5Gr4Exc77DoP9", 45 | "my custom pepper", Argon2Function.getInstance(4096, 20, 4, 32, Argon2.ID)) 46 | }; 47 | 48 | @Test 49 | public void test() 50 | { 51 | 52 | for (TestSuite test : PBKDF2_TEST) 53 | { 54 | Assert.assertEquals(test.hashingFunction.toString(), test.hash, 55 | Password.hash(test.password).addSalt(test.salt).addPepper(test.pepper).with(test.hashingFunction) 56 | .getResult()); 57 | 58 | SecureString securePassword = new SecureString(test.password.toCharArray()); 59 | Assert.assertEquals(test.hashingFunction.toString(), test.hash, 60 | Password.hash(securePassword).addSalt(test.salt).addPepper(test.pepper).with(test.hashingFunction) 61 | .getResult()); 62 | 63 | } 64 | 65 | } 66 | 67 | @Test 68 | public void testUpdate() 69 | { 70 | 71 | for (TestSuite test : PBKDF2_TEST) 72 | { 73 | Assert.assertEquals(test.hashingFunction.toString(), test.hash, 74 | Password.hash(test.password).addSalt(test.salt).addPepper(test.pepper).with(test.hashingFunction) 75 | .getResult()); 76 | 77 | HashUpdate update = Password.check(test.password, test.hash).addSalt(test.salt).addPepper(test.pepper).andUpdate().with(test.hashingFunction, test.hashingFunction); 78 | Assert.assertTrue(test.hashingFunction.toString(), update.isVerified()); 79 | Assert.assertEquals(test.hashingFunction.toString(), Password.hash(test.password).addSalt(test.salt).addPepper(test.pepper).with(test.hashingFunction).getResult(), update.getHash().getResult()); 80 | 81 | } 82 | 83 | } 84 | 85 | /** 86 | * Must compile. 87 | */ 88 | @Test 89 | public void testAccessibility() 90 | { 91 | try 92 | { 93 | String password = ""; 94 | String salt = ""; 95 | int saltLength = 1; 96 | String pepper = ""; 97 | 98 | HashBuilder hb = Password.hash(password); 99 | hb.addPepper(pepper); 100 | hb.addPepper(); 101 | hb.addRandomSalt(); 102 | hb.addRandomSalt(saltLength); 103 | hb.addSalt(salt); 104 | 105 | hb.withCompressedPBKDF2(); 106 | hb.withScrypt(); 107 | hb.withBcrypt(); 108 | hb.withPBKDF2(); 109 | hb.withArgon2(); 110 | 111 | HashChecker hc = Password.check(password, password); 112 | hc.addPepper(pepper); 113 | hc.addPepper(); 114 | hc.addSalt(salt); 115 | hc.withCompressedPBKDF2(); 116 | hc.withScrypt(); 117 | hc.withBcrypt(); 118 | hc.withPBKDF2(); 119 | hc.withArgon2(); 120 | 121 | Hmac.SHA256.code(); 122 | Hmac.values(); 123 | Hmac.SHA1.bits(); 124 | 125 | PBKDF2Function.getInstance(Hmac.SHA512, 1, 1); 126 | PBKDF2Function.getInstance(password, 1, 1); 127 | CompressedPBKDF2Function.getInstance(Hmac.SHA512, 1, 1); 128 | CompressedPBKDF2Function.getInstance(password, 1, 1); 129 | CompressedPBKDF2Function.getInstanceFromHash(password); 130 | 131 | BcryptFunction.getInstance(1); 132 | ScryptFunction.getInstance(2, 1, 1); 133 | ScryptFunction.getInstanceFromHash(password); 134 | 135 | Argon2Function.getInstance(8, 1, 1, 32, Argon2.ID); 136 | Argon2Function.getInstanceFromHash(password); 137 | 138 | SecureString s = new SecureString(new char[]{'a'}); 139 | s.clear(); 140 | } 141 | catch (Exception e) 142 | { 143 | // 144 | } 145 | } 146 | 147 | private static class TestSuite 148 | { 149 | private String hash; 150 | 151 | private String password; 152 | 153 | private String salt; 154 | 155 | private String pepper; 156 | 157 | private HashingFunction hashingFunction; 158 | 159 | TestSuite(String hash, String password, String salt, String pepper, HashingFunction hashingFunction) 160 | { 161 | this.hash = hash; 162 | this.password = password; 163 | this.salt = salt; 164 | this.pepper = pepper; 165 | this.hashingFunction = hashingFunction; 166 | } 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /src/test/com/password4j/StringTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.password4j; 19 | 20 | import org.junit.Assert; 21 | import org.junit.Test; 22 | 23 | import java.nio.CharBuffer; 24 | import java.util.Arrays; 25 | 26 | 27 | public class StringTest 28 | { 29 | @Test 30 | public void testConstructors() 31 | { 32 | // GIVEN 33 | char[] password = new char[] { 'a', 'b', 'c', 'd', 'e', 'f' }; 34 | 35 | // WHEN 36 | SecureString ss = new SecureString(password); 37 | CharSequence sub1 = ss.subSequence(1, 4); 38 | SecureString sub2 = new SecureString(password, 1, 4); 39 | 40 | // THEN 41 | Assert.assertEquals(sub1.length(), sub2.length()); 42 | Assert.assertEquals(Arrays.toString(new char[] { 'b', 'c', 'd' }), 43 | Arrays.toString(Utils.fromCharSequenceToChars(sub1))); 44 | Assert.assertEquals(Arrays.toString(Utils.fromCharSequenceToChars(sub1)), 45 | Arrays.toString(Utils.fromCharSequenceToChars(sub2))); 46 | 47 | } 48 | 49 | @Test 50 | public void testClear() 51 | { 52 | // GIVEN 53 | char[] password = new char[] { 'a', 'b', 'c', 'd' }; 54 | 55 | // WHEN 56 | SecureString ss = new SecureString(password); 57 | ss.clear(); 58 | 59 | // THEN 60 | char z = Character.MIN_VALUE; 61 | Assert.assertEquals(Arrays.toString(new char[] { z, z, z, z }), Arrays.toString(Utils.fromCharSequenceToChars(ss))); 62 | } 63 | 64 | @Test(expected = NullPointerException.class) 65 | public void testNull() 66 | { 67 | new SecureString(null); 68 | } 69 | 70 | @Test(expected = NullPointerException.class) 71 | public void testNull2() 72 | { 73 | new SecureString(null, 0, 0); 74 | } 75 | 76 | @Test(expected = ArrayIndexOutOfBoundsException.class) 77 | public void testOut1() 78 | { 79 | new SecureString(new char[] { 'a', 'b', 'c', 'd' }, 0, 10); 80 | } 81 | 82 | @Test(expected = NegativeArraySizeException.class) 83 | public void testOut2() 84 | { 85 | new SecureString(new char[] { 'a', 'b', 'c', 'd' }, 0, -4); 86 | } 87 | 88 | @Test(expected = NegativeArraySizeException.class) 89 | public void testOut3() 90 | { 91 | new SecureString(new char[] { 'a', 'b', 'c', 'd' }, 3, 0); 92 | } 93 | 94 | @Test 95 | public void testEmpty() 96 | { 97 | SecureString ss = new SecureString(new char[0]); 98 | 99 | Assert.assertEquals("SecureString[****]", ss.toString()); 100 | Assert.assertEquals(0, ss.length()); 101 | try 102 | { 103 | ss.charAt(0); 104 | Assert.fail(); 105 | } 106 | catch (ArrayIndexOutOfBoundsException e) 107 | { 108 | Assert.assertTrue(true); 109 | } 110 | 111 | } 112 | 113 | @Test 114 | public void testToString() 115 | { 116 | SecureString ss = new SecureString(new char[] { 'a', 'b', 'c', 'd' }); 117 | 118 | Assert.assertEquals("SecureString[****]", ss.toString()); 119 | } 120 | 121 | @Test 122 | public void erase() 123 | { 124 | // GIVEN 125 | char[] password1 = new char[] { 'a', 'b', 'c', 'd' }; 126 | char[] password2 = new char[] { 'a', 'b', 'c', 'd' }; 127 | 128 | // WHEN 129 | new SecureString(password1, true); 130 | new SecureString(password2, false); 131 | 132 | // THEN 133 | char z = Character.MIN_VALUE; 134 | Assert.assertEquals(Arrays.toString(new char[] { z, z, z, z }), Arrays.toString(password1)); 135 | Assert.assertEquals(Arrays.toString(new char[] { 'a', 'b', 'c', 'd' }), Arrays.toString(password2)); 136 | } 137 | 138 | @Test 139 | public void testEquality() 140 | { 141 | char[] password = new char[] { 'a', 'b', 'c', 'd' }; 142 | String str = new String(password); 143 | SecureString ss = new SecureString(password); 144 | SecureString ss2 = (SecureString) Utils.append(ss, "123"); 145 | 146 | Assert.assertTrue(((CharSequence) ss).equals(str) && ((CharSequence) ss).equals(CharBuffer.wrap(password))); 147 | Assert.assertNotEquals(null, ss); 148 | Assert.assertNotEquals("cbad", ss); 149 | Assert.assertEquals(new SecureString(password), ss); 150 | Assert.assertNotEquals(new SecureString(new char[] { 'b', 'b', 'b', 'b' }), ss); 151 | Assert.assertEquals(ss, ss); 152 | Assert.assertEquals(Arrays.hashCode(password), ss.hashCode()); 153 | 154 | Assert.assertNotEquals(ss, 123); 155 | Assert.assertNotEquals(ss2, ss); 156 | } 157 | 158 | @Test 159 | public void testUtilities() 160 | { 161 | char[] c1 = Utils.fromCharSequenceToChars(null); 162 | char[] c2 = Utils.fromCharSequenceToChars(new String(new char[0])); 163 | byte[] b1 = Utils.fromCharSequenceToBytes(null); 164 | byte[] b2 = Utils.fromCharSequenceToBytes(new String(new char[0])); 165 | byte[] b3 = Utils.fromCharSequenceToBytes(new String(new char[]{(char)1})); 166 | 167 | CharSequence cs1 = Utils.append("a", null); 168 | CharSequence cs2 = Utils.append(null, "b"); 169 | 170 | CharSequence a1 = Utils.append(new SecureString(new char[] { 'a', 'b', 'c' }), "def"); 171 | CharSequence a2 = Utils.append(null, "def"); 172 | CharSequence a3 = Utils.append(new SecureString(new char[0]), "def"); 173 | CharSequence a4 = Utils.append("abc", null); 174 | CharSequence a5 = Utils.append("abc", new SecureString(new char[0])); 175 | 176 | Assert.assertEquals(Arrays.toString(c1), Arrays.toString(c2)); 177 | Assert.assertEquals(Arrays.toString(b1), Arrays.toString(b2)); 178 | Assert.assertEquals(1, b3.length); 179 | Assert.assertEquals(1, b3[0]); 180 | Assert.assertEquals("a", cs1); 181 | Assert.assertEquals("b", cs2); 182 | Assert.assertEquals(Arrays.toString(new char[] { 'a', 'b', 'c', 'd', 'e', 'f' }), 183 | Arrays.toString(Utils.fromCharSequenceToChars(a1))); 184 | Assert.assertEquals(Arrays.toString(new char[] { 'd', 'e', 'f' }), 185 | Arrays.toString(Utils.fromCharSequenceToChars(a2))); 186 | Assert.assertEquals(Arrays.toString(new char[] { 'd', 'e', 'f' }), 187 | Arrays.toString(Utils.fromCharSequenceToChars(a3))); 188 | Assert.assertEquals(Arrays.toString(new char[] { 'a', 'b', 'c' }), 189 | Arrays.toString(Utils.fromCharSequenceToChars(a4))); 190 | Assert.assertEquals(Arrays.toString(new char[] { 'a', 'b', 'c' }), 191 | Arrays.toString(Utils.fromCharSequenceToChars(a5))); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/Password.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package com.password4j; 18 | 19 | 20 | /** 21 | * This class provides the two main operations on password: hash and verify. 22 | *

23 | * All the methods are static because there's no sense to 24 | * have an instance of {@link Password}. 25 | * It represents a facade layer that ease the usage of the 26 | * package itself and so this should be the only class 27 | * to be invoked from this package. 28 | * 29 | * @author David Bertoldi 30 | * @since 0.1.1 31 | */ 32 | public class Password 33 | { 34 | 35 | static 36 | { 37 | Utils.printBanner(System.out); //NOSONAR 38 | } 39 | 40 | private Password() 41 | { 42 | // 43 | } 44 | 45 | /** 46 | * Starts to hash the given plain text password. 47 | *

48 | * This method is used to start the setup of a {@link HashBuilder} 49 | * instance that finally should execute the {@link HashBuilder#with(HashingFunction)} 50 | * method to hash the password. 51 | * 52 | * @param plainTextPassword the plain text password 53 | * @return a builder instance of {@link HashBuilder} 54 | * @throws BadParametersException if any of the arguments are null. 55 | * @since 0.1.1 56 | */ 57 | public static HashBuilder hash(CharSequence plainTextPassword) 58 | { 59 | if (plainTextPassword == null) 60 | { 61 | throw new BadParametersException("Password cannot be null"); 62 | } 63 | return new HashBuilder(plainTextPassword); 64 | } 65 | 66 | /** 67 | * Starts to hash the given plain text password. 68 | *

69 | * This method is used to start the setup of a {@link HashBuilder} 70 | * instance that finally should execute the {@link HashBuilder#with(HashingFunction)} 71 | * method to hash the password. 72 | * 73 | * @param plainTextPassword the plain text password as bytes array 74 | * @return a builder instance of {@link HashBuilder} 75 | * @throws BadParametersException if any of the arguments are null. 76 | * @since 1.7.0 77 | */ 78 | public static HashBuilder hash(byte[] plainTextPassword) 79 | { 80 | if (plainTextPassword == null || plainTextPassword.length == 0) 81 | { 82 | throw new BadParametersException("Password cannot be null"); 83 | } 84 | return new HashBuilder(plainTextPassword); 85 | } 86 | 87 | 88 | /** 89 | * Starts to verify if a hash string has been generated with 90 | * the given plain text password. 91 | *

92 | * This method is used to start the setup of an {@link HashChecker} 93 | * instance that finally should execute the {@link HashChecker#with(HashingFunction)} 94 | * method to verify the hash. 95 | * 96 | * @param plainTextPassword the plain text password 97 | * @param hash a hash string 98 | * @return a builder instance of {@link HashChecker} 99 | * @throws BadParametersException if any of the arguments are null. 100 | * @since 0.1.1 101 | */ 102 | public static HashChecker check(CharSequence plainTextPassword, String hash) 103 | { 104 | if (hash == null || plainTextPassword == null) 105 | { 106 | throw new BadParametersException("Hash or plain cannot be null"); 107 | } 108 | return new HashChecker(plainTextPassword, hash); 109 | } 110 | 111 | 112 | /** 113 | * Starts to verify if a hash string has been generated with 114 | * the given plain text password. 115 | *

116 | * This method is used to start the setup of an {@link HashChecker} 117 | * instance that finally should execute the {@link HashChecker#with(HashingFunction)} 118 | * method to verify the hash. 119 | * 120 | * @param plainTextPassword the plain text password as bytes array 121 | * @param hash a hash string as bytes array 122 | * @return a builder instance of {@link HashChecker} 123 | * @throws BadParametersException if any of the arguments are null. 124 | * @since 1.7.0 125 | */ 126 | public static HashChecker check(byte[] plainTextPassword, byte[] hash) 127 | { 128 | if (hash == null || plainTextPassword == null || hash.length == 0 || plainTextPassword.length == 0) 129 | { 130 | throw new BadParametersException("Hash or plain cannot be null"); 131 | } 132 | return new HashChecker(plainTextPassword, hash); 133 | } 134 | 135 | /** 136 | * Starts to verify if a hash object has been generated with 137 | * the given plain text password. 138 | *

139 | * This method uses the {@link HashingFunction} used to calculate the given {@link Hash}. 140 | * Il the password is null, this returns false; 141 | * otherwise {@link HashingFunction#check(CharSequence, String)} is invoked. 142 | * 143 | * @param plainTextPassword the original password. 144 | * @param hashObject an {@link Hash} object. 145 | * @return true if the check passes, false otherwise. 146 | * @throws BadParametersException if the Hash is null or if there's no hashing function defined in it. 147 | * @since 1.0.3 148 | */ 149 | public static boolean check(CharSequence plainTextPassword, Hash hashObject) 150 | { 151 | return check(Utils.fromCharSequenceToBytes(plainTextPassword), hashObject); 152 | } 153 | 154 | /** 155 | * Starts to verify if a hash object has been generated with 156 | * the given plain text password. 157 | *

158 | * This method uses the {@link HashingFunction} used to calculate the given {@link Hash}. 159 | * Il the password is null, this returns false; 160 | * otherwise {@link HashingFunction#check(CharSequence, String)} is invoked. 161 | * 162 | * @param plainTextPassword the original password as bytes array. 163 | * @param hashObject an {@link Hash} object. 164 | * @return true if the check passes, false otherwise. 165 | * @throws BadParametersException if the Hash is null or if there's no hashing function defined in it. 166 | * @since 1.7.0 167 | */ 168 | public static boolean check(byte[] plainTextPassword, Hash hashObject) 169 | { 170 | if (hashObject == null || hashObject.getHashingFunction() == null) 171 | { 172 | throw new BadParametersException("Invalid Hash object. " + (hashObject != null ? hashObject.toString() : null)); 173 | } 174 | if (plainTextPassword == null) 175 | { 176 | return false; 177 | } 178 | 179 | return hashObject.getHashingFunction().check(plainTextPassword, hashObject.getResultAsBytes(), hashObject.getSaltBytes(), hashObject.getPepper()); 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/MessageDigestFunction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package com.password4j; 18 | 19 | import java.security.MessageDigest; 20 | import java.security.NoSuchAlgorithmException; 21 | import java.util.Map; 22 | import java.util.Objects; 23 | import java.util.concurrent.ConcurrentHashMap; 24 | 25 | 26 | /** 27 | * Class containing the implementation of Messaged digest functions provided by {@link MessageDigest}. 28 | * 29 | * @author David Bertoldi 30 | * @see Message digests 31 | * @since 1.4.0 32 | */ 33 | public class MessageDigestFunction extends AbstractHashingFunction 34 | { 35 | protected static final SaltOption DEFAULT_SALT_OPTION = SaltOption.APPEND; 36 | private static final Map INSTANCES = new ConcurrentHashMap<>(); 37 | private final String algorithm; 38 | 39 | private final SaltOption saltOption; 40 | 41 | 42 | MessageDigestFunction(String algorithm, SaltOption saltOption) 43 | { 44 | this.algorithm = algorithm; 45 | this.saltOption = saltOption; 46 | } 47 | 48 | /** 49 | * Creates a singleton instance, depending on the provided 50 | * algorithm, number of iterations and key length. 51 | * 52 | * @param algorithm message digest algorithm 53 | * @return a singleton instance 54 | * @since 1.4.0 55 | */ 56 | public static MessageDigestFunction getInstance(String algorithm) 57 | { 58 | return getInstance(algorithm, DEFAULT_SALT_OPTION); 59 | } 60 | 61 | /** 62 | * Creates a singleton instance, depending on the provided 63 | * algorithm, number of iterations and key length. 64 | * 65 | * @param algorithm hmac algorithm 66 | * @param saltOption a configuration that specifies how the salt is concatenated to the plain text password 67 | * @return a singleton instance 68 | * @since 1.4.0 69 | */ 70 | public static MessageDigestFunction getInstance(String algorithm, SaltOption saltOption) 71 | { 72 | String key = getUID(algorithm, saltOption); 73 | if (INSTANCES.containsKey(key)) 74 | { 75 | return INSTANCES.get(key); 76 | } 77 | else 78 | { 79 | MessageDigestFunction function = new MessageDigestFunction(algorithm, saltOption); 80 | INSTANCES.put(key, function); 81 | return function; 82 | } 83 | } 84 | 85 | protected static String getUID(String algorithm, SaltOption saltOption) 86 | { 87 | return algorithm + "|" + saltOption.name(); 88 | } 89 | 90 | protected static String toString(String algorithm, SaltOption saltOption) 91 | { 92 | return "a=" + algorithm + ", o=" + saltOption.name(); 93 | } 94 | 95 | @Override 96 | public Hash hash(CharSequence plainTextPassword) 97 | { 98 | return hash(plainTextPassword, null); 99 | } 100 | 101 | @Override 102 | public Hash hash(byte[] plainTextPasswordAsBytes) 103 | { 104 | return hash(plainTextPasswordAsBytes, null); 105 | } 106 | 107 | @Override 108 | public Hash hash(CharSequence plainTextPassword, String salt) 109 | { 110 | return internalHash(Utils.fromCharSequenceToBytes(plainTextPassword), Utils.fromCharSequenceToBytes(salt)); 111 | } 112 | 113 | @Override 114 | public Hash hash(byte[] plainTextPasswordAsBytes, byte[] saltAsBytes) 115 | { 116 | return internalHash(plainTextPasswordAsBytes, saltAsBytes); 117 | } 118 | 119 | protected Hash internalHash(byte[] plainTextPassword, byte[] salt) 120 | { 121 | byte[] finalCharSequence = concatenateSalt(plainTextPassword, salt); 122 | 123 | byte[] result = getMessageDigest().digest(finalCharSequence); 124 | return new Hash(this, Utils.toHex(result), result, salt); 125 | } 126 | 127 | protected MessageDigest getMessageDigest() 128 | { 129 | try 130 | { 131 | return MessageDigest.getInstance(algorithm); 132 | } 133 | catch (NoSuchAlgorithmException nsae) 134 | { 135 | throw new UnsupportedOperationException("`" + algorithm + "` is not supported by your system.", nsae); 136 | } 137 | } 138 | 139 | @Override 140 | public boolean check(CharSequence plainTextPassword, String hashed) 141 | { 142 | return check(plainTextPassword, hashed, null); 143 | } 144 | 145 | @Override 146 | public boolean check(byte[] plainTextPasswordAsBytes, byte[] hashed) 147 | { 148 | return check(plainTextPasswordAsBytes, hashed, null); 149 | } 150 | 151 | @Override 152 | public boolean check(CharSequence plainTextPassword, String hashed, String salt) 153 | { 154 | Hash hash = internalHash(Utils.fromCharSequenceToBytes(plainTextPassword), Utils.fromCharSequenceToBytes(salt)); 155 | return slowEquals(hash.getResult(), hashed); 156 | } 157 | 158 | @Override 159 | public boolean check(byte[] plainTextPassword, byte[] hashed, byte[] salt) 160 | { 161 | Hash hash = internalHash(plainTextPassword, salt); 162 | return slowEquals(hash.getResultAsBytes(), hashed); 163 | } 164 | 165 | /** 166 | * The salt option describes if the Salt is appended or prepended to 167 | * the plain text password. 168 | * 169 | * @return how the salt is concatenated 170 | * @since 1.5.1 171 | */ 172 | public SaltOption getSaltOption() 173 | { 174 | return saltOption; 175 | } 176 | 177 | /** 178 | * The algorithm in use by this instance. 179 | * 180 | * @return the algorithm in use 181 | * @since 1.5.1 182 | */ 183 | public String getAlgorithm() 184 | { 185 | return algorithm; 186 | } 187 | 188 | 189 | private byte[] concatenateSalt(byte[] plainTextPassword, byte[] salt) 190 | { 191 | if (salt == null || salt.length == 0) 192 | { 193 | return plainTextPassword; 194 | } 195 | 196 | if (saltOption == SaltOption.PREPEND) 197 | { 198 | return Utils.append(salt, plainTextPassword); 199 | } 200 | return Utils.append(plainTextPassword, salt); 201 | } 202 | 203 | @Override 204 | public String toString() 205 | { 206 | return getClass().getSimpleName() + '(' + toString(this.algorithm, this.saltOption) + ')'; 207 | } 208 | 209 | @Override 210 | public boolean equals(Object o) 211 | { 212 | if (this == o) 213 | return true; 214 | if (!(o instanceof MessageDigestFunction)) 215 | return false; 216 | MessageDigestFunction other = (MessageDigestFunction) o; 217 | return algorithm.equals(other.algorithm) // 218 | && saltOption == other.saltOption; 219 | } 220 | 221 | @Override 222 | public int hashCode() 223 | { 224 | return Objects.hash(algorithm, saltOption); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/CompressedPBKDF2Function.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.password4j; 19 | 20 | import com.password4j.types.Hmac; 21 | 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.concurrent.ConcurrentHashMap; 25 | 26 | 27 | /** 28 | * Class containing the implementation of PBKDF2 function and its parameters. 29 | *

30 | * The main difference between {@link PBKDF2Function} is the hash produced: the configurations of the CHF, 31 | * the salt and the hash are encoded inside it. 32 | *

33 | * The produced hash is in the form 34 | *

35 | * 36 | * $algorithm$parameters$salt$hash 37 | * 38 | *

39 | * Assuming {@code $} as delimiter. 40 | *

41 | *

    42 | *
  • 43 | * The algorithm is encoded with its numeric uid {@link Hmac#code()} 44 | *
  • 45 | *
  • 46 | * Parameters are encoded in one integer where the length occupies the first 32bit and 47 | * the number of iterations the remaining 32 bits. 48 | *
  • 49 | *
  • 50 | * Salt is encoded in Base64 51 | *
  • 52 | *
  • 53 | * Hash is encoded in Base64 as in {@link PBKDF2Function} 54 | *
  • 55 | *
56 | * 57 | * @author David Bertoldi 58 | * @see PBKDF2 59 | * @since 0.1.0 60 | */ 61 | public class CompressedPBKDF2Function extends PBKDF2Function 62 | { 63 | 64 | private static final Map INSTANCES = new ConcurrentHashMap<>(); 65 | 66 | private static final char DELIMITER = PropertyReader.readChar("hash.pbkdf2.delimiter", '$'); 67 | 68 | protected CompressedPBKDF2Function() 69 | { 70 | super(); 71 | } 72 | 73 | protected CompressedPBKDF2Function(Hmac fromCode, int iterations, int length) 74 | { 75 | super(fromCode, iterations, length); 76 | } 77 | 78 | /** 79 | * Creates a singleton instance, depending on the provided 80 | * algorithm, number of iterations and key length. 81 | * 82 | * @param algorithm hmac algorithm 83 | * @param iterations number of iterations 84 | * @param length length of the derived key 85 | * @return a singleton instance 86 | * @since 0.1.0 87 | */ 88 | public static CompressedPBKDF2Function getInstance(Hmac algorithm, int iterations, int length) 89 | { 90 | String key = getUID(algorithm.name(), iterations, length); 91 | if (INSTANCES.containsKey(key)) 92 | { 93 | return INSTANCES.get(key); 94 | } 95 | else 96 | { 97 | CompressedPBKDF2Function function = new CompressedPBKDF2Function(algorithm, iterations, length); 98 | INSTANCES.put(key, function); 99 | return function; 100 | } 101 | } 102 | 103 | /** 104 | * Creates a singleton instance, depending on the provided 105 | * algorithm, number of iterations and key length. 106 | * 107 | * @param algorithm string version of hmac algorithm. This must me mapped in {@link Hmac}. 108 | * @param iterations number of iterations 109 | * @param length length of the derived key 110 | * @return a singleton instance 111 | * @throws IllegalArgumentException if {@code algorithm} is not mapped in {@link Hmac}. 112 | * @since 0.1.0 113 | */ 114 | public static CompressedPBKDF2Function getInstance(String algorithm, int iterations, int length) 115 | { 116 | try 117 | { 118 | return getInstance(Hmac.valueOf(algorithm), iterations, length); 119 | } 120 | catch (IllegalArgumentException iae) 121 | { 122 | throw new UnsupportedOperationException("Algorithm `" + algorithm + "` is not recognized.", iae); 123 | } 124 | } 125 | 126 | /** 127 | * Reads the configuration contained in the given hash and 128 | * builds a singleton instance based on these configurations. 129 | * 130 | * @param hashed an already hashed password 131 | * @return a singleton instance based on the given hash 132 | * @since 1.0.0 133 | */ 134 | public static CompressedPBKDF2Function getInstanceFromHash(String hashed) 135 | { 136 | String[] parts = getParts(hashed); 137 | if (parts.length == 5) 138 | { 139 | int algorithm = Integer.parseInt(parts[1]); 140 | long configuration = Long.parseLong(parts[2]); 141 | 142 | int iterations = (int) (configuration >> 32); 143 | int length = (int) configuration; 144 | 145 | return CompressedPBKDF2Function.getInstance(Hmac.fromCode(algorithm), iterations, length); 146 | } 147 | throw new BadParametersException("`" + hashed + "` is not a valid hash"); 148 | } 149 | 150 | protected static List getParts(byte[] hashed) 151 | { 152 | return Utils.split(hashed, (byte) DELIMITER); 153 | } 154 | 155 | protected static String[] getParts(String hashed) 156 | { 157 | String regex = "\\" + DELIMITER; 158 | return hashed.split(regex); 159 | } 160 | 161 | @Override 162 | protected String getHash(byte[] encodedKey, byte[] salt) 163 | { 164 | String params = Long.toString((((long) getIterations()) << 32) | (getLength() & 0xffffffffL)); 165 | String salt64 = Utils.encodeBase64(salt); 166 | String hash64 = super.getHash(encodedKey, salt); 167 | return "$" + algorithm.code() + "$" + params + "$" + salt64 + "$" + hash64; 168 | } 169 | 170 | @Override 171 | public boolean check(CharSequence plainTextPassword, String hashed) 172 | { 173 | return check(Utils.fromCharSequenceToBytes(plainTextPassword), Utils.fromCharSequenceToBytes(hashed)); 174 | } 175 | 176 | @Override 177 | public boolean check(byte[] plainTextPassword, byte[] hashed) 178 | { 179 | byte[] salt = getSaltFromHash(hashed); 180 | Hash internalHas = hash(plainTextPassword, salt); 181 | 182 | return slowEquals(internalHas.getResultAsBytes(), hashed); 183 | } 184 | 185 | @Override 186 | public boolean check(CharSequence plainTextPassword, String hashed, String salt) 187 | { 188 | byte[] hashAsBytes = Utils.fromCharSequenceToBytes(hashed); 189 | byte[] realSalt = getSaltFromHash(hashAsBytes); 190 | byte[] plainTextPasswordAsBytes = Utils.fromCharSequenceToBytes(plainTextPassword); 191 | Hash internalHash = hash(plainTextPasswordAsBytes, realSalt); 192 | return slowEquals(internalHash.getResult(), hashed); 193 | } 194 | 195 | @Override 196 | public boolean check(byte[] plainTextPassword, byte[] hashed, byte[] salt) 197 | { 198 | byte[] realSalt = getSaltFromHash(hashed); 199 | Hash internalHash = hash(plainTextPassword, realSalt); 200 | return slowEquals(internalHash.getResultAsBytes(), hashed); 201 | } 202 | 203 | private byte[] getSaltFromHash(byte[] hashed) 204 | { 205 | List parts = getParts(hashed); 206 | if (parts.size() == 5) 207 | { 208 | return Utils.decodeBase64(parts.get(3)); 209 | } 210 | throw new BadParametersException("`" + Utils.fromBytesToString(hashed) + "` is not a valid hash"); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.password4j 6 | password4j 7 | 1.8.4 8 | jar 9 | 10 | 11 | Password4j 12 | 13 | Java cryptographic library that supports Argon2, bcrypt, scrypt and PBKDF2 aimed to protect passwords in databases. 14 | Easy to use by design, highly customizable, secure and portable. 15 | All the implementations follow the standards and have been reviewed to perform better in the JVM. 16 | 17 | https://password4j.com/ 18 | 19 | 20 | 21 | scm:git:git@github.com/Password4j/${project.artifactId}.git 22 | scm:git:ssh://git@github.com/Password4j/${project.artifactId}.git 23 | https://github.com/Password4j/password4j 24 | 1.8.4 25 | 26 | 27 | 28 | 29 | Apache License, Version 2.0 30 | http://www.apache.org/licenses/LICENSE-2.0.txt 31 | repo 32 | 33 | 34 | 35 | 36 | 37 | David Bertoldi 38 | dav.bertoldi@gmail.com 39 | 40 | Java Developer 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.slf4j 49 | slf4j-api 50 | 2.0.12 51 | 52 | 53 | 54 | junit 55 | junit 56 | 4.13.2 57 | test 58 | 59 | 60 | 61 | org.slf4j 62 | slf4j-simple 63 | 2.0.12 64 | test 65 | 66 | 67 | 68 | 69 | 70 | 71 | ossrh 72 | https://oss.sonatype.org/content/repositories/snapshots 73 | 74 | 75 | ossrh 76 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 77 | 78 | 79 | 80 | 81 | src/test 82 | 83 | 84 | org.codehaus.mojo 85 | animal-sniffer-maven-plugin 86 | 1.23 87 | 88 | 89 | net.sf.androidscents.signature 90 | android-api-level-21 91 | 5.0.1_r2 92 | 93 | 94 | 95 | 96 | org.apache.maven.plugins 97 | maven-compiler-plugin 98 | 3.12.1 99 | 100 | 8 101 | 8 102 | 103 | 104 | 105 | 106 | org.jacoco 107 | jacoco-maven-plugin 108 | 0.8.11 109 | 110 | 111 | 112 | prepare-agent 113 | 114 | 115 | 116 | report 117 | test 118 | 119 | report 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | release 131 | 132 | 133 | 134 | org.apache.maven.plugins 135 | maven-gpg-plugin 136 | 3.0.1 137 | 138 | 139 | sign-artifacts 140 | verify 141 | 142 | sign 143 | 144 | 145 | 146 | 147 | 148 | 149 | org.sonatype.central 150 | central-publishing-maven-plugin 151 | 0.7.0 152 | true 153 | 154 | central 155 | 156 | 157 | 158 | 159 | org.apache.maven.plugins 160 | maven-source-plugin 161 | 3.2.1 162 | 163 | 164 | attach-sources 165 | 166 | jar-no-fork 167 | 168 | 169 | 170 | 171 | 172 | 173 | org.apache.maven.plugins 174 | maven-release-plugin 175 | 2.5.3 176 | 177 | true 178 | false 179 | release 180 | deploy 181 | 182 | 183 | 184 | 185 | org.apache.maven.plugins 186 | maven-javadoc-plugin 187 | 2.9.1 188 | 189 | 190 | attach-javadocs 191 | 192 | jar 193 | 194 | 195 | -Xdoclint:none 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | UTF-8 207 | UTF-8 208 | password4j 209 | https://sonarcloud.io 210 | 211 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /src/test/com/password4j/ScryptFunctionTest.java: -------------------------------------------------------------------------------- 1 | package com.password4j; 2 | 3 | import com.password4j.types.Bcrypt; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | 7 | import java.util.Base64; 8 | import java.util.Properties; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | 12 | 13 | public class ScryptFunctionTest 14 | { 15 | 16 | @Test(expected = BadParametersException.class) 17 | public void testBadHash() 18 | { 19 | // GIVEN 20 | String badHash = "bad$hash&"; 21 | 22 | // WHEN 23 | ScryptFunction.getInstanceFromHash(badHash); 24 | 25 | } 26 | 27 | @Test(expected = BadParametersException.class) 28 | public void testNullPassword() 29 | { 30 | // GIVEN 31 | ScryptFunction scrypt = ScryptFunction.getInstance(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); 32 | 33 | // WHEN 34 | scrypt.hash("password", "salt"); 35 | 36 | } 37 | 38 | @Test 39 | public void testHash1() 40 | { 41 | // GIVEN 42 | String password = "password"; 43 | String salt = "salt"; 44 | 45 | // WHEN 46 | Hash hash = new ScryptFunction(16384, 8, 1).hash(password, salt); 47 | String result = hash.getResult(); 48 | byte[] bytes = hash.getBytes(); 49 | 50 | // THEN 51 | String expected = "$e0801$c2FsdA==$dFcxr0SE8yOWiWntoomu7gBbWQOsVh5kpayhIXl793NO+f1YQi4uIhg7ysup7Ie6DIO3oueI8Dzg2gZGNDPNpg=="; 52 | byte[] expectedBytes = Base64.getDecoder().decode(expected.split("\\$")[3]); 53 | Assert.assertEquals(expected, result); 54 | Assert.assertArrayEquals(expectedBytes, bytes); 55 | 56 | } 57 | 58 | @Test 59 | public void testHash2() 60 | { 61 | // GIVEN 62 | String password = "password"; 63 | 64 | // WHEN 65 | boolean result = new ScryptFunction(16384, 8, 1) 66 | .check(password, "$e0801$c2FsdA==$dFcxr0SE8yOWiWntoomu7gBbWQOsVh5kpayhIXl793NO+f1YQi4uIhg7ysup7Ie6DIO3oueI8Dzg2gZGNDPNpg=="); 67 | 68 | // THEN 69 | Assert.assertTrue(result); 70 | } 71 | 72 | @Test 73 | public void testHash3() 74 | { 75 | // GIVEN 76 | String password = "password"; 77 | String salt = "salt"; 78 | 79 | // WHEN 80 | boolean result = new ScryptFunction(16384, 8, 1).check(password, "$e0801$c2FsdA==$c2FsdA=="); 81 | 82 | // THEN 83 | Assert.assertFalse(result); 84 | } 85 | 86 | @Test 87 | public void testHashRandomSalt() 88 | { 89 | // GIVEN 90 | String password = "password"; 91 | ScryptFunction sCryptFunction = new ScryptFunction(16384, 8, 1); 92 | 93 | // WHEN 94 | Hash hash = sCryptFunction.hash(password); 95 | 96 | // THEN 97 | Assert.assertTrue(hash.getSalt() != null && hash.getSalt().length() > 0); 98 | Assert.assertEquals(sCryptFunction, ScryptFunction.getInstanceFromHash(hash.getResult())); 99 | } 100 | 101 | @Test 102 | public void testWrongCheck() 103 | { 104 | // GIVEN 105 | String password = "password"; 106 | String salt = "salt"; 107 | 108 | // WHEN 109 | Hash hash = new ScryptFunction(16384, 8, 1).hash(password, salt); 110 | 111 | // THEN 112 | Assert.assertFalse(hash.getHashingFunction().check(password, "$e0801$c2FsdA==$YXNkYXNkYXNkYXNk")); 113 | } 114 | 115 | @Test 116 | public void testEquality() 117 | { 118 | // GIVEN 119 | int r = 1; 120 | int N = 2; 121 | int p = 3; 122 | ScryptFunction scrypt = ScryptFunction.getInstance(N, r, p); 123 | 124 | // THEN 125 | boolean eqNull = scrypt.equals(null); 126 | boolean eqClass = scrypt.equals(new BcryptFunction(Bcrypt.A, 10)); 127 | boolean difInst = scrypt.equals(ScryptFunction.getInstance(5, 4, 6)); 128 | boolean sameInst = scrypt.equals(ScryptFunction.getInstance(N, r, p)); 129 | boolean sameInst2 = scrypt.equals(new ScryptFunction(N, r, p)); 130 | String toString = scrypt.toString(); 131 | int hashCode = scrypt.hashCode(); 132 | boolean notSameInst1 = scrypt.equals(new ScryptFunction(N + 1, r, p)); 133 | boolean notSameInst2 = scrypt.equals(new ScryptFunction(N, r + 1 + 1, p)); 134 | boolean notSameInst3 = scrypt.equals(new ScryptFunction(N, r, p + 1)); 135 | 136 | // END 137 | Assert.assertFalse(eqNull); 138 | Assert.assertFalse(eqClass); 139 | Assert.assertFalse(difInst); 140 | Assert.assertTrue(sameInst); 141 | Assert.assertTrue(sameInst2); 142 | Assert.assertNotEquals(toString, new ScryptFunction(5, 4, 6).toString()); 143 | Assert.assertNotEquals(hashCode, new ScryptFunction(5, 4, 6).hashCode()); 144 | Assert.assertFalse(notSameInst1); 145 | Assert.assertFalse(notSameInst2); 146 | Assert.assertFalse(notSameInst3); 147 | } 148 | 149 | @Test 150 | public void testResources() 151 | { 152 | // GIVEN 153 | 154 | // WHEN 155 | ScryptFunction scrypt1 = ScryptFunction.getInstance(5, 3, 7); 156 | ScryptFunction scrypt2 = ScryptFunction.getInstance(25, 30, 14); 157 | ScryptFunction scrypt3 = ScryptFunction.getInstance(1, 1, 1); 158 | 159 | // THEN 160 | Assert.assertEquals(13_440, scrypt1.getRequiredBytes()); 161 | Assert.assertTrue(scrypt1.getRequiredMemory().indexOf("KB") > 0); 162 | Assert.assertEquals(1_344_000, scrypt2.getRequiredBytes()); 163 | Assert.assertTrue(scrypt2.getRequiredMemory().indexOf("MB") > 0); 164 | Assert.assertEquals(128, scrypt3.getRequiredBytes()); 165 | Assert.assertEquals("128B", scrypt3.getRequiredMemory()); 166 | } 167 | 168 | @Test(expected = BadParametersException.class) 169 | public void testBadParameters1() 170 | { 171 | // GIVEN 172 | int r = 5; 173 | 174 | // WHEN 175 | ScryptFunction.getInstance((16777215 / r) + 1, r, 1).hash("password"); 176 | } 177 | 178 | @Test(expected = BadParametersException.class) 179 | public void testBadParameters2() 180 | { 181 | // GIVEN 182 | int p = 5; 183 | 184 | // WHEN 185 | ScryptFunction.getInstance(16, (16777215 / p) + 1, p).hash("password"); 186 | } 187 | 188 | @Test(expected = BadParametersException.class) 189 | public void testBadParameters3() 190 | { 191 | // GIVEN 192 | int k = 16777215; 193 | 194 | // WHEN 195 | ScryptFunction.getInstance(2, 2 << 20, 16777215).hash("password"); 196 | } 197 | 198 | @Test(expected = BadParametersException.class) 199 | public void testBadParameters4() 200 | { 201 | // GIVEN 202 | 203 | // WHEN 204 | ScryptFunction.getInstance(1, 4, 3).hash("password"); 205 | } 206 | 207 | @Test(expected = BadParametersException.class) 208 | public void testBadParameters5() 209 | { 210 | // GIVEN 211 | 212 | // WHEN 213 | ScryptFunction.getInstance(50, 4, 3).hash("password"); 214 | } 215 | 216 | @Test(expected = BadParametersException.class) 217 | public void testBadParameters6() 218 | { 219 | // GIVEN 220 | int p = 5; 221 | 222 | // WHEN 223 | new ScryptFunction(16384, 8, 1).check("password", "$s1$e0801$c2FsdA==$dFcxr0SE8yOWiWntoomu7gBbWQOsVh5kpayhIXl793NO+f1YQi4uIhg7ysup7Ie6DIO3oueI8Dzg2gZGNDPNpg=="); 224 | } 225 | 226 | @Test(expected = BadParametersException.class) 227 | public void testBadParameters7() 228 | { 229 | // GIVEN 230 | 231 | // WHEN 232 | new ScryptFunction(16384, 8, 1).check("password", "$e0801c2FsdA==$dFcxr0SE8yOWiWntoomu7gBbWQOsVh5kpayhIXl793NO+f1YQi4uIhg7ysup7Ie6DIO3oueI8Dzg2gZGNDPNpg=="); 233 | } 234 | 235 | @Test 236 | public void testAccessors() 237 | { 238 | // GIVEN 239 | int workFactor = 3; 240 | int resources = 5; 241 | int parallelization = 7; 242 | int derivedKeyLength = 32; 243 | 244 | // WHEN 245 | ScryptFunction scrypt = ScryptFunction.getInstance(workFactor, resources, parallelization, derivedKeyLength); 246 | 247 | // THEN 248 | Assert.assertEquals(workFactor, scrypt.getWorkFactor()); 249 | Assert.assertEquals(resources, scrypt.getResources()); 250 | Assert.assertEquals(parallelization, scrypt.getParallelization()); 251 | Assert.assertEquals(derivedKeyLength, scrypt.getDerivedKeyLength()); 252 | Assert.assertEquals("ScryptFunction(N=3, r=5, p=7, l=32)", scrypt.toString()); 253 | } 254 | 255 | @Test 256 | public void testOWASP() 257 | { 258 | // GIVEN 259 | Properties oldProps = PropertyReader.properties; 260 | PropertyReader.properties = null; 261 | 262 | // WHEN 263 | ScryptFunction scrypt = AlgorithmFinder.getScryptInstance(); 264 | 265 | // THEN 266 | assertEquals(1 << 16, scrypt.getWorkFactor()); 267 | assertEquals(8, scrypt.getResources()); 268 | assertEquals(1, scrypt.getParallelization()); 269 | assertEquals(ScryptFunction.DERIVED_KEY_LENGTH, scrypt.getDerivedKeyLength()); 270 | 271 | PropertyReader.properties = oldProps; 272 | } 273 | 274 | 275 | 276 | } 277 | -------------------------------------------------------------------------------- /src/test/com/password4j/IssuesTest.java: -------------------------------------------------------------------------------- 1 | package com.password4j; 2 | 3 | import com.password4j.types.Argon2; 4 | import org.junit.Assert; 5 | import org.junit.Assume; 6 | import org.junit.Test; 7 | 8 | import java.lang.reflect.Field; 9 | import java.security.Provider; 10 | import java.security.Security; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.Set; 14 | import java.util.concurrent.ConcurrentMap; 15 | import java.util.stream.Collectors; 16 | 17 | import static org.junit.Assert.*; 18 | 19 | public class IssuesTest 20 | { 21 | 22 | /** 23 | * @see issue #92 24 | */ 25 | @Test 26 | public void issue92() 27 | { 28 | String hash = "$argon2id$v=19$m=16384,t=2,p=1$nlm7oNI5zquzSYkyby6oVw$JOkJAYrDB0i2gmiJrXC6o2r+u1rszCm/RO9gIQtnxlY"; 29 | String plain = "Test123!"; 30 | Argon2Function function = Argon2Function.getInstanceFromHash(hash); 31 | 32 | boolean verified = Password.check(plain, hash).with(function); 33 | Hash newHash = Password.hash(plain).addSalt("Y9ΫI2o.W").with(function); 34 | boolean verified2 = Password.check(plain, newHash); 35 | 36 | assertTrue(verified); 37 | assertTrue(verified2); 38 | assertEquals("$argon2id$v=19$m=16384,t=2,p=1$WTnOq0kyby5X$SewIdM+Ywctw0lfNQ0xKYoUIlyRs3qF+gVmEVtpdmyg", newHash.getResult()); 39 | } 40 | 41 | 42 | /** 43 | * @see issue #99 44 | */ 45 | @Test 46 | public void issue99() 47 | { 48 | int memory = 65536; 49 | int iterations = 2; 50 | int parallelism = 3; 51 | int outputLength = 32; 52 | int version = 0x13; 53 | byte[] salt = 54 | { 55 | (byte) 0x6b, (byte) 0x25, (byte) 0xc9, (byte) 0xd7, (byte) 0x0e, (byte) 0x5c, (byte) 0x19, (byte) 0xac, 56 | (byte) 0x51, (byte) 0x74, (byte) 0xd7, (byte) 0x74, (byte) 0x53, (byte) 0xad, (byte) 0x23, (byte) 0x70, 57 | (byte) 0x15, (byte) 0x27, (byte) 0x56, (byte) 0x2e, (byte) 0x02, (byte) 0xb8, (byte) 0xec, (byte) 0x5c, 58 | (byte) 0xac, (byte) 0x89, (byte) 0x2d, (byte) 0xc3, (byte) 0xe4, (byte) 0xb5, (byte) 0x1c, (byte) 0x12 59 | }; 60 | byte[] password="Test".getBytes(); 61 | Argon2 type = Argon2.ID; 62 | Argon2Function instance=Argon2Function.getInstance(memory, iterations, parallelism, outputLength, type, version); 63 | 64 | Hash hash = instance.hash(password, salt); 65 | 66 | 67 | String expResult = "cbcfdee482c233e525ca405c7014e89cd33142758a2f1d23c420690f950c988c"; 68 | assertEquals(expResult, printBytesToString(hash.getBytes())); 69 | } 70 | 71 | /** 72 | * @see issue #93 73 | */ 74 | @Test 75 | public void issue93() 76 | { 77 | String hash = "$argon2id$v=19$m=16384,t=2,p=1$nlm7oNI5zquzSYkyby6oVw$JOkJAYrDB0i2gmiJrXC6o2r+u1rszCm/RO9gIQtnxlY"; 78 | Argon2Function function = Argon2Function.getInstanceFromHash(hash); 79 | 80 | boolean test1 = Password.check("Test123!", hash).with(function); 81 | assertTrue(test1); 82 | 83 | boolean test2 = function.check("Test123!", hash); 84 | assertTrue(test2); 85 | } 86 | 87 | 88 | /** 89 | * @see issue #120 90 | */ 91 | @Test(expected = Test.None.class) 92 | public void issue120() 93 | { 94 | // GIVEN 95 | String name = "issue120FakeProvider"; 96 | Provider emptyProvider = new Provider(name, 1, "info") 97 | { 98 | @Override 99 | public synchronized Set getServices() 100 | { 101 | return null; 102 | } 103 | }; 104 | Security.addProvider(emptyProvider); 105 | 106 | // WHEN 107 | Password.hash("hash"); 108 | 109 | // THEN 110 | Security.removeProvider(name); 111 | } 112 | 113 | 114 | /** 115 | * @see issue #126 116 | */ 117 | @Test 118 | public void issue126() 119 | { 120 | byte[] hashBytes = Password.hash("’(っ^▿^)۶\uD83C\uDF78\uD83C\uDF1F\uD83C\uDF7A٩(˘◡˘ ) ❌❌ ❌❌❌") 121 | .addSalt("\uD83E\uDDC2") 122 | .withScrypt() 123 | .getBytes(); 124 | 125 | Assert.assertEquals("827b022b411e712e5ae4855d8c71cb047d882b2457120d1019974d17dcf6f1bf59644d9a93e470ab14ee5f7a88ae9b0140d2db121de58f6d830fc9c16c82f212", printBytesToString(hashBytes)); 126 | 127 | 128 | hashBytes = Password.hash("ŸŁĀPRČ") 129 | .addSalt("ŸŁĀPRČAA") 130 | .withArgon2() 131 | .getBytes(); 132 | 133 | Assert.assertEquals("59dedcf45d7a8604926ca66f6abe3990ce8b6ba108f535836fa18e95b7d94e9f56301e422c1d487dd06dc26061261402a5f7fe912bd545b6aeec866fec74df81", printBytesToString(hashBytes)); 134 | 135 | } 136 | 137 | @Test 138 | public void issue162() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException 139 | { 140 | // GIVEN 141 | String versionProperty = System.getProperty("java.specification.version"); 142 | if(!versionProperty.contains(".")) 143 | { 144 | int major = Integer.parseInt(versionProperty); 145 | Assume.assumeTrue(major < 17 ); 146 | } 147 | 148 | Argon2Function argon2Function = Argon2Function.getInstance(1, 1, 2, 32, Argon2.I); 149 | 150 | // WHEN 151 | Password.hash("password").with(argon2Function); 152 | 153 | // THEN 154 | Class clazz = Class.forName("java.lang.ApplicationShutdownHooks"); 155 | Field field = clazz.getDeclaredField("hooks"); 156 | field.setAccessible(true); 157 | List hooks = ((Map) field.get(null)).values().stream().filter(thread -> "password4j-shutdownhook".equals(thread.getName())).collect(Collectors.toList()); 158 | 159 | Assert.assertFalse(hooks.isEmpty()); 160 | } 161 | 162 | 163 | private static String printBytesToString(byte[] bytes) 164 | { 165 | StringBuilder byteString= new StringBuilder(); 166 | if (bytes!=null) 167 | { 168 | for (byte aByte : bytes) 169 | { 170 | byteString.append(String.format("%02x", aByte)); 171 | } 172 | } 173 | else 174 | { 175 | byteString = new StringBuilder("-"); 176 | } 177 | return byteString.toString(); 178 | } 179 | 180 | /** 181 | *

Issue #163: Tomcat Thread Leak Prevention

182 | * 183 | *

Problem: Tomcat was reporting thread leak warnings:

184 | *
185 | * "The web application appears to have started a thread named [password4j-worker-X] 186 | * but has failed to stop it. This is very likely to create a memory leak." 187 | *
188 | * 189 | *

Root Cause: Non-daemon threads prevent JVM shutdown and cause 190 | * application server warnings when web applications are undeployed.

191 | * 192 | *

Solution: Changed thread.setDaemon(false) to 193 | * thread.setDaemon(true) in Utils.createExecutorService()

194 | * 195 | *

Impact: Daemon threads automatically terminate when the main 196 | * application shuts down, preventing memory leaks in containerized environments.

197 | * 198 | * @see GitHub Issue #163 199 | * @see Utils#createExecutorService() 200 | * @see Oracle: Daemon Threads 201 | * @since 1.8.1 202 | * @category Threading 203 | * @category MemoryLeak 204 | */ 205 | 206 | @Test 207 | public void issue163() { 208 | final String plainText = "The quick brown fox jumps over the lazy dog"; 209 | final String hash = Password.hash(plainText) 210 | .withArgon2() 211 | .getResult(); 212 | assertNotNull("Password hashing should work", hash); 213 | try { 214 | Thread.sleep(200); 215 | } catch (InterruptedException e) { 216 | Thread.currentThread().interrupt(); 217 | } 218 | 219 | boolean foundWorkerThread = false; 220 | for (Thread thread : Thread.getAllStackTraces().keySet()) { 221 | if (thread.getName().startsWith("password4j-worker-")) { 222 | foundWorkerThread = true; 223 | 224 | // THE FIX: This thread MUST be daemon to prevent Tomcat memory leak warning 225 | final String assertionMessage = "Thread " + thread.getName() + " must be daemon thread to prevent Tomcat leak warning"; 226 | assertTrue(assertionMessage, thread.isDaemon()); 227 | } 228 | } 229 | assertTrue("Should have found at least one password4j-worker thread", foundWorkerThread); 230 | } 231 | 232 | } 233 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/PBKDF2Function.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package com.password4j; 18 | 19 | import com.password4j.types.Hmac; 20 | 21 | import javax.crypto.SecretKey; 22 | import javax.crypto.SecretKeyFactory; 23 | import javax.crypto.spec.PBEKeySpec; 24 | import java.security.NoSuchAlgorithmException; 25 | import java.security.spec.InvalidKeySpecException; 26 | import java.util.Arrays; 27 | import java.util.Map; 28 | import java.util.Objects; 29 | import java.util.concurrent.ConcurrentHashMap; 30 | 31 | 32 | /** 33 | * Class containing the implementation of PBKDF2 function and its parameters. 34 | * 35 | * @author David Bertoldi 36 | * @see PBKDF2 37 | * @since 0.1.0 38 | */ 39 | public class PBKDF2Function extends AbstractHashingFunction 40 | { 41 | private static final Map INSTANCES = new ConcurrentHashMap<>(); 42 | 43 | private static final String ALGORITHM_PREFIX = "PBKDF2WithHmac"; 44 | 45 | protected Hmac algorithm; 46 | 47 | protected String algorithmAsString; 48 | 49 | protected int iterations; 50 | 51 | protected int length; 52 | 53 | protected PBKDF2Function() 54 | { 55 | // 56 | } 57 | 58 | protected PBKDF2Function(int iterations, int length) 59 | { 60 | this.iterations = iterations; 61 | this.length = length; 62 | } 63 | 64 | protected PBKDF2Function(Hmac algorithm, int iterations, int length) 65 | { 66 | this(iterations, length); 67 | this.algorithm = algorithm; 68 | this.algorithmAsString = algorithm.name(); 69 | } 70 | 71 | protected PBKDF2Function(String algorithm, int iterations, int length) 72 | { 73 | this(iterations, length); 74 | this.algorithmAsString = algorithm; 75 | } 76 | 77 | /** 78 | * Creates a singleton instance, depending on the provided 79 | * algorithm, number of iterations and key length. 80 | * 81 | * @param algorithm hmac algorithm 82 | * @param iterations number of iterations 83 | * @param length length of the derived key 84 | * @return a singleton instance 85 | * @since 0.1.0 86 | */ 87 | public static PBKDF2Function getInstance(Hmac algorithm, int iterations, int length) 88 | { 89 | return getInstance(algorithm.name(), iterations, length); 90 | } 91 | 92 | /** 93 | * Creates a singleton instance, depending on the provided 94 | * algorithm, number of iterations and key length. 95 | * 96 | * @param algorithm string version of hmac algorithm 97 | * @param iterations number of iterations 98 | * @param length length of the derived key 99 | * @return a singleton instance 100 | * @since 0.1.0 101 | */ 102 | public static PBKDF2Function getInstance(String algorithm, int iterations, int length) 103 | { 104 | String key = getUID(algorithm, iterations, length); 105 | if (INSTANCES.containsKey(key)) 106 | { 107 | return INSTANCES.get(key); 108 | } 109 | else 110 | { 111 | PBKDF2Function function = new PBKDF2Function(algorithm, iterations, length); 112 | INSTANCES.put(key, function); 113 | return function; 114 | } 115 | } 116 | 117 | protected static SecretKey internalHash(byte[] plainTextPassword, byte[] salt, String algorithm, int iterations, int length) throws NoSuchAlgorithmException, InvalidKeySpecException 118 | { 119 | if (salt == null) 120 | { 121 | throw new IllegalArgumentException("Salt cannot be null"); 122 | } 123 | return internalHash(Utils.fromBytesToChars(plainTextPassword), salt, algorithm, iterations, length); 124 | } 125 | 126 | protected static SecretKey internalHash(char[] plain, byte[] salt, String algorithm, int iterations, int length) 127 | throws NoSuchAlgorithmException, InvalidKeySpecException 128 | { 129 | SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(ALGORITHM_PREFIX + algorithm); 130 | PBEKeySpec spec = new PBEKeySpec(plain, salt, iterations, length); 131 | try 132 | { 133 | return secretKeyFactory.generateSecret(spec); 134 | } 135 | finally 136 | { 137 | spec.clearPassword(); 138 | } 139 | } 140 | 141 | protected static String getUID(String algorithm, int iterations, int length) 142 | { 143 | return algorithm + "|" + iterations + "|" + length; 144 | } 145 | 146 | protected static String toString(String algorithm, int iterations, int length) 147 | { 148 | return "a=" + algorithm + ", i=" + iterations + ", l=" + length; 149 | } 150 | 151 | @Override 152 | public Hash hash(CharSequence plainTextPassword) 153 | { 154 | byte[] salt = SaltGenerator.generate(); 155 | return hash(Utils.fromCharSequenceToBytes(plainTextPassword), salt); 156 | } 157 | 158 | @Override 159 | public Hash hash(byte[] plainTextPasswordAsBytes) 160 | { 161 | byte[] salt = SaltGenerator.generate(); 162 | return hash(plainTextPasswordAsBytes, salt); 163 | } 164 | 165 | @Override 166 | public Hash hash(CharSequence plainTextPassword, String salt) 167 | { 168 | return hash(Utils.fromCharSequenceToBytes(plainTextPassword), Utils.fromCharSequenceToBytes(salt)); 169 | } 170 | 171 | @Override 172 | public Hash hash(byte[] plainTextPassword, byte[] salt) 173 | { 174 | try 175 | { 176 | SecretKey key = internalHash(plainTextPassword, salt, this.algorithmAsString, this.iterations, this.length); 177 | byte[] encodedKey = key.getEncoded(); 178 | return new Hash(this, getHash(encodedKey, salt), encodedKey, salt); 179 | } 180 | catch (NoSuchAlgorithmException nsae) 181 | { 182 | String message = "`" + algorithm + "` is not a valid algorithm"; 183 | throw new UnsupportedOperationException(message, nsae); 184 | } 185 | catch (IllegalArgumentException | InvalidKeySpecException e) 186 | { 187 | String message = "Invalid specification with salt=" + Arrays.toString(salt) + ", iterations=" + iterations + " and length=" + length; 188 | throw new BadParametersException(message, e); 189 | } 190 | } 191 | 192 | /** 193 | * Overridable PBKDF2 generator 194 | * 195 | * @param encodedKey secret encodedKey 196 | * @param salt cryptographic salt 197 | * @return the PBKDF2 hash string 198 | */ 199 | protected String getHash(byte[] encodedKey, byte[] salt) 200 | { 201 | return Utils.encodeBase64(encodedKey); 202 | } 203 | 204 | @Override 205 | public boolean check(CharSequence plainTextPassword, String hashed) 206 | { 207 | return check((byte[]) null, null); 208 | } 209 | 210 | @Override 211 | public boolean check(byte[] plainTextPasswordAsBytes, byte[] hashed) 212 | { 213 | throw new UnsupportedOperationException("This implementation requires an explicit salt."); 214 | 215 | } 216 | 217 | @Override 218 | public boolean check(CharSequence plainTextPassword, String hashed, String salt) 219 | { 220 | Hash internalHash = hash(plainTextPassword, salt); 221 | return slowEquals(internalHash.getResult(), hashed); 222 | } 223 | 224 | @Override 225 | public boolean check(byte[] plainTextPasswordAsBytes, byte[] hashed, byte[] salt) 226 | { 227 | Hash internalHash = hash(plainTextPasswordAsBytes, salt); 228 | return slowEquals(internalHash.getResultAsBytes(), hashed); 229 | } 230 | 231 | 232 | public String getAlgorithm() 233 | { 234 | return algorithmAsString; 235 | } 236 | 237 | public int getIterations() 238 | { 239 | return iterations; 240 | } 241 | 242 | public int getLength() 243 | { 244 | return length; 245 | } 246 | 247 | @Override 248 | public boolean equals(Object obj) 249 | { 250 | if (obj == null || !getClass().equals(obj.getClass())) 251 | { 252 | return false; 253 | } 254 | 255 | PBKDF2Function otherStrategy = (PBKDF2Function) obj; 256 | return this.algorithmAsString.equals(otherStrategy.algorithmAsString) // 257 | && this.iterations == otherStrategy.iterations // 258 | && this.length == otherStrategy.length; 259 | } 260 | 261 | @Override 262 | public String toString() 263 | { 264 | return getClass().getSimpleName() + '(' + toString(this.algorithmAsString, this.iterations, this.length) + ')'; 265 | } 266 | 267 | @Override 268 | public int hashCode() 269 | { 270 | return Objects.hash(algorithmAsString, iterations, length); 271 | } 272 | 273 | } 274 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | # [1.8.4](https://github.com/Password4j/password4j/releases/tag/1.8.4) - (2025-06-30) 4 | ### Fixed 5 | * Daemon Threads belonging to the parallelization pool are now shutdown during JVM shutdown in order to prevent memory leaks ([#163](../../issues/163)) 6 | 7 | # [1.8.3](https://github.com/Password4j/password4j/releases/tag/1.8.3) - (2025-05-08) 8 | ### Fixed 9 | * Threads belonging to the parallelization pool are now shutdown during JVM shutdown in order to prevent memory leaks ([#162](../../issues/162)) 10 | 11 | 12 | # [1.8.2](https://github.com/Password4j/password4j/releases/tag/1.8.2) - (2024-05-01) 13 | ### Fixed 14 | * Multi thread algorithms use daemon threads in order to not block the application shutdown if there is no explicit `System.exit()` ([#151](../../issues/151)) 15 | * Password4j works even when there is no access to `psw4j.properties` file due to restrictive security policies ([#153](../../issues/153)) 16 | 17 | 18 | ## [1.8.1](https://github.com/Password4j/password4j/releases/tag/1.8.1) - (2024-03-08) 19 | ### Fixed 20 | * `Argon2Function#internalHash(...)` used a double conversion from `byte[]` to `String` and back to `byte[]` that created inconsistencies in `Hash#salt ([#143](../../issues/143)) 21 | 22 | ### Changed 23 | * `Hash#Hash(HashingFunction, String, byte[], String)` marked deprecated 24 | 25 | ## [1.8.0](https://github.com/Password4j/password4j/releases/tag/1.8.0) - (2024-03-03) 26 | ### Added 27 | * Balloon Hashing implementation ([#131](../../issues/131)) 28 | 29 | ### Fixed 30 | * Parallelism is achieved by an `ExecutorService` instantiated during object creation instead of during the hashing process 31 | 32 | ### Changed 33 | * Banner is disabled by default 34 | 35 | ## [1.7.3](https://github.com/Password4j/password4j/releases/tag/1.7.3) - (2023-09-14) 36 | ### Fixed 37 | * Wrong hashes when the password contains non ISO 8859-1 characters ([#126](../../issues/126)) 38 | 39 | 40 | ## [1.7.2](https://github.com/Password4j/password4j/releases/tag/1.7.2) - (2023-08-20) 41 | ### Fixed 42 | * Suppressed warning for usage of `java.security.AccessController`. This is how the java development team fixed the problem for the moment ([#119](../../issues/119)) 43 | * In some custom JDK implementations `java.security.Provider#getServices()` can return `null` instead of empty `java.security.Provider.Service[]` ([#120](../../issues/120)) 44 | 45 | 46 | ## [1.7.1](https://github.com/Password4j/password4j/releases/tag/1.7.1) - (2023-06-02) 47 | ### Fixed 48 | * Bcrypt used negative rounds when cost factor = 31 ([#114](../../issues/114)) 49 | 50 | ## [1.7.0](https://github.com/Password4j/password4j/releases/tag/1.7.0) - (2023-02-18) 51 | ### Added 52 | * APIs now accepts `byte[]` arguments ([#99](../../issues/99)) 53 | * `.forceUpdate()` forces Password4j to recalculate a new hash even if the parameters didn't change ([#102](../../issues/102)) 54 | * configurable salt length with property `global.salt.length` when using `#addRandomSalt()` ([#97](../../issues/97)) 55 | ### Changed 56 | * `.andUpdate()` no more recalculations of the hash if the algorithm, salt or pepper changed from the hash found in `Password.check()` ([#102](../../issues/102)) 57 | ### Fixed 58 | * Inconsistency between public and internal APIs for Argon2 ([#93](../../issues/93)) 59 | 60 | ## [1.6.3](https://github.com/Password4j/password4j/releases/tag/1.6.3) - (2022-12-08) 61 | ### Fixed 62 | * Inconsistency of Argon2 with some kind of salts generated from external libraries [#92](../../issues/92) 63 | 64 | ## [1.6.2](https://github.com/Password4j/password4j/releases/tag/1.6.2) - (2022-10-20) 65 | ### Added 66 | * Application banner ([#83](../../issues/83)). 67 | ### Fixed 68 | * Typo for issue [#80](../../issues/80) 69 | ### Removed 70 | * Dependencies to Apache Commons Text, which had been vulnerable to arbitrary code execution in the past ([#84](../../issues/84)). 71 | 72 | ## [1.6.1](https://github.com/Password4j/password4j/releases/tag/1.6.1) - (2022-10-07) 73 | ### Changed 74 | * Algorithms' default values are aligned to [OWASP recommendation](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) ([#80](../../issues/80)) 75 | 76 | ## [1.6.0](https://github.com/Password4j/password4j/releases/tag/1.6.0) - (2022-06-17) 77 | ### Changed 78 | * `BCryptFunction`, `SCryptFunction`, `#withBCrypt()`, `#withSCrypt()`, `getBCryptInstance()`, `getSCryptInstance()` to `BcryptFunction`, `ScryptFunction`, `#withBcrypt()`, `#withScrypt()`, `getBcryptInstance()`, `getScryptInstance()` ([#36](../../issues/36)). 79 | ### Fixed 80 | * Scrypt never prepends `$s0` to the result ([#64](../../issues/64)). 81 | 82 | ## [1.5.4](https://github.com/Password4j/password4j/releases/tag/1.5.4) - (2021-11-19) 83 | ### Fixed 84 | * Removed `slf4j-nop` which can cause issues if not excluded from the dependency tree ([#46](../../issues/46)) 85 | 86 | ## [1.5.3](https://github.com/Password4j/password4j/releases/tag/1.5.3) - (2021-04-14) 87 | ### Fixed 88 | * `byte[]` are converted to `String` with environment-based encoding instead of UTF-8 ([#35](../../issues/35) and [#16](../../issues/16)). 89 | 90 | ## [1.5.2](https://github.com/Password4j/password4j/releases/tag/1.5.2) - (2021-02-21) 91 | ### Changed 92 | * Raised the compatibility with Android API level from 26+ (Android 8.0) to 21+ (Android 5.0). 93 | * `SystemChecker`'s benchmark tools returns a prototype of the function and the real elapsed time ([#23](../../issues/23)). 94 | ### Fixed 95 | * Argon2 was not using the given pepper with `Password.check(String, Hash)`. 96 | * Salt was converted from `String` to `byte[]` too many times. ([#31](../../issues/31)). 97 | ### Removed 98 | * Dependency with Apache Commons Text. 99 | 100 | ## [1.5.1](https://github.com/Password4j/password4j/releases/tag/1.5.1) - (2021-02-05) 101 | ### Added 102 | * `Hash` stores the byte array containing the calculated hash without algorithm's parameters and salt ([#26](../../issues/26)). 103 | ### Changed 104 | * Scrypt accepts dynamic key length ([#24](../../issues/24)). 105 | ### Fixed 106 | * Improved `toString()` methods' readability. 107 | 108 | ## [1.5.0](https://github.com/Password4j/password4j/releases/tag/1.5.0) - (2021-02-02) 109 | ### Added 110 | * Argon2 support. 111 | ### Changed 112 | * Enums `BCrypt` and `Hmac` are moved from `com.password4j` to `com.password4j.types`. 113 | ### Fixed 114 | * Some typos along the code. 115 | 116 | ## [1.4.0](https://github.com/Password4j/password4j/releases/tag/1.4.0) - (2020-11-15) 117 | ### Added 118 | * CHFs like MD5, SHA-1, SHA-2 and SHA-3 in order to increase compatibility with legacy systems. 119 | ### Changed 120 | * `PBKDF2Function.getAlgorithm()` returns a `String` instead of an `Hmac` enum. This make `PBKDF2Function.toString()` and `CompressedPBKDF2Function.toString()` more readable. 121 | * `SystemChecker.isPBKDF2Supported()` accepts a `String` instead of an `Hmac` enum. 122 | ### Fixed 123 | * Some typos along the code. 124 | 125 | ## [1.3.2](https://github.com/Password4j/password4j/releases/tag/1.3.2) - (2020-09-09) 126 | ### Fixed 127 | * The location of the configuration file is now customizable ([#5](../../issues/5)). 128 | ### Security 129 | * `SecureString.toString()` now hides the length of the string ([#6](../../issues/6)). 130 | 131 | ## [1.3.1](https://github.com/Password4j/password4j/releases/tag/1.3.1) - (2020-03-25) 132 | ### Fixed 133 | * `toString()` of some `HashingFunction` produced non-unique output ([#3](../../issues/3)). 134 | * added missing getters for some `HashingFunction`s ([#4](../../issues/4)). 135 | 136 | ## [1.3.0](https://github.com/Password4j/password4j/releases/tag/1.3.0) - (2020-03-19) 137 | ### Added 138 | * Capability of updating the hash (re-hash) with a new configuration just after the verification process. 139 | ### Changed 140 | * `HashBuilder` and `HashChecker` are less extendable because there are more maintainability issues than effective advantages 141 | * Pepper can be provided either with `SecureString` or `String`. 142 | ### Removed 143 | * `Password.hash()` and `Password.check()` methods that accepts a custom `HashBuilder` or a custom `HashChecker`. 144 | 145 | ## [1.2.1](https://github.com/Password4j/password4j/releases/tag/1.2.1) - (2020-03-17) 146 | ### Added 147 | * Constant time equality in `SecureString`. 148 | ### Changed 149 | * Enum `WithHmac` renamed to `Hmac`. 150 | 151 | ## [1.2.0](https://github.com/Password4j/password4j/releases/tag/1.2.0) - (2020-03-15) 152 | ### Added 153 | * This CHANGELOG.md file. 154 | ### Security 155 | * Plain text passwords can be provided either with `SecureString` or `String`. 156 | 157 | ## [1.1.0](https://github.com/Password4j/password4j/releases/tag/1.1.0) - (2020-03-14) 158 | ### Added 159 | * Configurable delimiter for `CompressedPBKDF2Function` (before was `$`). 160 | ### Removed 161 | * `Hash.check()` method because `Password.check()` should be the only way to verify passwords. 162 | ### Fixed 163 | * Values from `psw4j.properties` are not properly cached. 164 | * Typos in README.md 165 | * Typos in `SystemChecker`'s methods' signature. 166 | 167 | ## [1.0.2](https://github.com/Password4j/password4j/releases/tag/1.0.2) - (2020-03-12) 168 | ### Change 169 | * `SystemChecker.java` has no more a `main` method but must be called from end user's code. 170 | Removed UI and execution from Maven profile. 171 | 172 | ## [1.0.1](https://github.com/Password4j/password4j/releases/tag/password4j-1.0.1) - (2020-03-11) 173 | ### Change 174 | * POM structure and dependencies. 175 | 176 | ## [1.0.0](https://github.com/Password4j/password4j/releases/tag/1.0.0) - (2020-03-11) 177 | ### Change 178 | * API are more readable for end users. 179 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/Blake2b.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package com.password4j; 18 | 19 | import java.util.Arrays; 20 | 21 | 22 | class Blake2b 23 | { 24 | private static final long[] IV = {0x6a09e667f3bcc908L, 0xbb67ae8584caa73bL, 0x3c6ef372fe94f82bL, 0xa54ff53a5f1d36f1L, 25 | 0x510e527fade682d1L, 0x9b05688c2b3e6c1fL, 0x1f83d9abfb41bd6bL, 0x5be0cd19137e2179L}; 26 | 27 | private static final byte[][] SIGMA = {{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, 28 | {14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3}, {11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4}, 29 | {7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8}, {9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13}, 30 | {2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9}, {12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11}, 31 | {13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10}, {6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5}, 32 | {10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0}, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, 33 | {14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3}}; 34 | 35 | private static final int ROUNDS = 12; 36 | 37 | private static final int BLOCK_LENGTH_BYTES = 128; 38 | 39 | private final int digestLength; 40 | 41 | private final int keyLength; 42 | 43 | private final byte[] buffer; 44 | 45 | private final long[] internalState = new long[16]; 46 | 47 | private int bufferPos = 0; 48 | 49 | private long[] chainValue = null; 50 | 51 | private long t0 = 0L; 52 | 53 | private long t1 = 0L; 54 | 55 | private long f0 = 0L; 56 | 57 | /** 58 | * Basic sized constructor - size in bytes. 59 | * 60 | * @param digestSize size of the digest in bytes 61 | */ 62 | Blake2b(int digestSize) 63 | { 64 | if (digestSize < 1 || digestSize > 64) 65 | { 66 | throw new BadParametersException("BLAKE2b digest bytes length must be not greater than 64"); 67 | } 68 | 69 | buffer = new byte[BLOCK_LENGTH_BYTES]; 70 | keyLength = 0; 71 | this.digestLength = digestSize; 72 | init(); 73 | } 74 | 75 | // initialize chainValue 76 | private void init() 77 | { 78 | chainValue = new long[8]; 79 | chainValue[0] = IV[0] ^ (digestLength | ((long) keyLength << 8) | 0x1010000); 80 | chainValue[1] = IV[1]; 81 | chainValue[2] = IV[2]; 82 | chainValue[3] = IV[3]; 83 | chainValue[4] = IV[4]; 84 | chainValue[5] = IV[5]; 85 | chainValue[6] = IV[6]; 86 | chainValue[7] = IV[7]; 87 | } 88 | 89 | private void initializeInternalState() 90 | { 91 | System.arraycopy(chainValue, 0, internalState, 0, chainValue.length); 92 | System.arraycopy(IV, 0, internalState, chainValue.length, 4); 93 | internalState[12] = t0 ^ IV[4]; 94 | internalState[13] = t1 ^ IV[5]; 95 | internalState[14] = f0 ^ IV[6]; 96 | internalState[15] = IV[7];// ^ f1 with f1 = 0 97 | } 98 | 99 | void update(byte[] message) 100 | { 101 | if (message == null) 102 | { 103 | return; 104 | } 105 | update(message, 0, message.length); 106 | } 107 | 108 | /** 109 | * update the message digest with a block of bytes. 110 | * 111 | * @param message the byte array containing the data. 112 | * @param offset the offset into the byte array where the data starts. 113 | * @param len the length of the data. 114 | */ 115 | void update(byte[] message, int offset, int len) 116 | { 117 | int remainingLength = 0; 118 | 119 | if (bufferPos != 0) 120 | { 121 | remainingLength = BLOCK_LENGTH_BYTES - bufferPos; 122 | if (remainingLength < len) 123 | { 124 | System.arraycopy(message, offset, buffer, bufferPos, remainingLength); 125 | t0 += BLOCK_LENGTH_BYTES; 126 | if (t0 == 0) 127 | { 128 | t1++; 129 | } 130 | compress(buffer, 0); 131 | bufferPos = 0; 132 | Arrays.fill(buffer, (byte) 0);// clear buffer 133 | } 134 | else 135 | { 136 | System.arraycopy(message, offset, buffer, bufferPos, len); 137 | bufferPos += len; 138 | return; 139 | } 140 | } 141 | 142 | int messagePos; 143 | int blockWiseLastPos = offset + len - BLOCK_LENGTH_BYTES; 144 | for (messagePos = offset + remainingLength; messagePos < blockWiseLastPos; messagePos += BLOCK_LENGTH_BYTES) 145 | { 146 | t0 += BLOCK_LENGTH_BYTES; 147 | if (t0 == 0) 148 | { 149 | t1++; 150 | } 151 | compress(message, messagePos); 152 | } 153 | 154 | // fill the buffer with left bytes, this might be a full block 155 | System.arraycopy(message, messagePos, buffer, 0, offset + len - messagePos); 156 | bufferPos += offset + len - messagePos; 157 | } 158 | 159 | /** 160 | * close the digest, producing the final digest value. The doFinal 161 | * call leaves the digest reset. 162 | * Key, salt and personal string remain. 163 | * 164 | * @param out the array the digest is to be copied into. 165 | * @param outOffset the offset into the out array the digest is to start at. 166 | */ 167 | void doFinal(byte[] out, int outOffset) 168 | { 169 | 170 | f0 = 0xFFFFFFFFFFFFFFFFL; 171 | t0 += bufferPos; 172 | if (bufferPos > 0 && t0 == 0) 173 | { 174 | t1++; 175 | } 176 | compress(buffer, 0); 177 | Arrays.fill(buffer, (byte) 0);// Holds eventually the key if input is null 178 | Arrays.fill(internalState, 0L); 179 | 180 | for (int i = 0; i < chainValue.length && (i * 8 < digestLength); i++) 181 | { 182 | byte[] bytes = Utils.longToLittleEndian(chainValue[i]); 183 | 184 | if (i * 8 < digestLength - 8) 185 | { 186 | System.arraycopy(bytes, 0, out, outOffset + i * 8, 8); 187 | } 188 | else 189 | { 190 | System.arraycopy(bytes, 0, out, outOffset + i * 8, digestLength - (i * 8)); 191 | } 192 | } 193 | 194 | Arrays.fill(chainValue, 0L); 195 | 196 | reset(); 197 | } 198 | 199 | /** 200 | * Reset the digest back to it's initial state. 201 | * The key, the salt and the personal string will 202 | * remain for further computations. 203 | */ 204 | void reset() 205 | { 206 | bufferPos = 0; 207 | f0 = 0L; 208 | t0 = 0L; 209 | t1 = 0L; 210 | chainValue = null; 211 | Arrays.fill(buffer, (byte) 0); 212 | init(); 213 | } 214 | 215 | private void compress(byte[] message, int messagePos) 216 | { 217 | 218 | initializeInternalState(); 219 | 220 | long[] m = new long[16]; 221 | for (int j = 0; j < 16; j++) 222 | { 223 | m[j] = Utils.littleEndianToLong(message, messagePos + j * 8); 224 | } 225 | 226 | for (int round = 0; round < ROUNDS; round++) 227 | { 228 | 229 | // G apply to columns of internalState:m[blake2b_sigma[round][2 * 230 | // blockPos]] /+1 231 | functionG(m[SIGMA[round][0]], m[SIGMA[round][1]], 0, 4, 8, 12); 232 | functionG(m[SIGMA[round][2]], m[SIGMA[round][3]], 1, 5, 9, 13); 233 | functionG(m[SIGMA[round][4]], m[SIGMA[round][5]], 2, 6, 10, 14); 234 | functionG(m[SIGMA[round][6]], m[SIGMA[round][7]], 3, 7, 11, 15); 235 | // G apply to diagonals of internalState: 236 | functionG(m[SIGMA[round][8]], m[SIGMA[round][9]], 0, 5, 10, 15); 237 | functionG(m[SIGMA[round][10]], m[SIGMA[round][11]], 1, 6, 11, 12); 238 | functionG(m[SIGMA[round][12]], m[SIGMA[round][13]], 2, 7, 8, 13); 239 | functionG(m[SIGMA[round][14]], m[SIGMA[round][15]], 3, 4, 9, 14); 240 | } 241 | 242 | // update chain values: 243 | for (int offset = 0; offset < chainValue.length; offset++) 244 | { 245 | chainValue[offset] = chainValue[offset] ^ internalState[offset] ^ internalState[offset + 8]; 246 | } 247 | } 248 | 249 | private void functionG(long m1, long m2, int posA, int posB, int posC, int posD) 250 | { 251 | 252 | internalState[posA] = internalState[posA] + internalState[posB] + m1; 253 | internalState[posD] = Long.rotateRight(internalState[posD] ^ internalState[posA], 32); 254 | internalState[posC] = internalState[posC] + internalState[posD]; 255 | internalState[posB] = Long.rotateRight(internalState[posB] ^ internalState[posC], 24); // replaces 25 of BLAKE 256 | internalState[posA] = internalState[posA] + internalState[posB] + m2; 257 | internalState[posD] = Long.rotateRight(internalState[posD] ^ internalState[posA], 16); 258 | internalState[posC] = internalState[posC] + internalState[posD]; 259 | internalState[posB] = Long.rotateRight(internalState[posB] ^ internalState[posC], 63); // replaces 11 of BLAKE 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/HashBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package com.password4j; 18 | 19 | /** 20 | * Builder class that helps to create a chain of parameters to be used 21 | * in the hashing process. 22 | * 23 | * @author David Bertoldi 24 | * @since 1.0.0 25 | */ 26 | public class HashBuilder 27 | { 28 | protected byte[] salt; 29 | protected CharSequence pepper; 30 | private byte[] plainTextPassword; 31 | 32 | @SuppressWarnings("unused") 33 | private HashBuilder() 34 | { 35 | // 36 | } 37 | 38 | /** 39 | * @param plainTextPassword the plain text password 40 | * @since 1.0.0 41 | */ 42 | protected HashBuilder(CharSequence plainTextPassword) 43 | { 44 | this.plainTextPassword = Utils.fromCharSequenceToBytes(plainTextPassword); 45 | } 46 | 47 | /** 48 | * @param plainTextPasswordAsBytes the plain text password as bytes array 49 | * @since 1.7.0 50 | */ 51 | protected HashBuilder(byte[] plainTextPasswordAsBytes) 52 | { 53 | this.plainTextPassword = plainTextPasswordAsBytes; 54 | } 55 | 56 | /** 57 | * Add a cryptographic salt in the hashing process. 58 | * The salt is applied differently depending on the chosen algorithm. 59 | * 60 | * @param salt cryptographic salt 61 | * @return this builder 62 | * @since 1.0.0 63 | */ 64 | public HashBuilder addSalt(String salt) 65 | { 66 | this.salt = Utils.fromCharSequenceToBytes(salt); 67 | return this; 68 | } 69 | 70 | /** 71 | * Add a cryptographic salt in the hashing process. 72 | * The salt is applied differently depending on the chosen algorithm. 73 | * 74 | * @param saltAsBytes cryptographic salt as bytes array 75 | * @return this builder 76 | * @since 1.7.0 77 | */ 78 | public HashBuilder addSalt(byte[] saltAsBytes) 79 | { 80 | this.salt = saltAsBytes; 81 | return this; 82 | } 83 | 84 | /** 85 | * Add a random cryptographic salt in the hashing process. 86 | * The salt is applied differently depending on the chosen algorithm. 87 | *

88 | * Calling this method can be omitted for all the CHFs that require a salt. 89 | * 90 | * @return this builder 91 | * @see SaltGenerator#generate() for more information about the length of the product 92 | * @since 1.0.0 93 | */ 94 | public HashBuilder addRandomSalt() 95 | { 96 | this.salt = SaltGenerator.generate(); 97 | return this; 98 | } 99 | 100 | /** 101 | * Add a random cryptographic salt in the hashing process with a given length. 102 | * The salt is applied differently depending on the chosen algorithm. 103 | * 104 | * @param length the length of the salt produced 105 | * @return this builder 106 | * @throws BadParametersException if the length is non-positive 107 | * @see SaltGenerator#generate() for more information about the length of the product 108 | * @since 1.0.0 109 | */ 110 | public HashBuilder addRandomSalt(int length) 111 | { 112 | if (length <= 0) 113 | { 114 | throw new BadParametersException("Salt cannot have a non-positive length"); 115 | } 116 | else 117 | { 118 | this.salt = SaltGenerator.generate(length); 119 | } 120 | return this; 121 | } 122 | 123 | /** 124 | * Concatenates the pepper configured in your `psw4j.properties` file with the plain text password. 125 | * The produced sequence (in the form {@code pepper+password}) is processed by the algorithm. 126 | * 127 | * @return this builder 128 | * @see PepperGenerator#get() 129 | */ 130 | public HashBuilder addPepper() 131 | { 132 | this.pepper = PepperGenerator.get(); 133 | return this; 134 | } 135 | 136 | /** 137 | * Concatenates the provided string with the plain text password. 138 | * The produced sequence (in the form {@code pepper+password}) is processed by the algorithm. 139 | * 140 | * @param pepper cryptographic pepper 141 | * @return this builder 142 | * @since 1.0.0 143 | */ 144 | public HashBuilder addPepper(CharSequence pepper) 145 | { 146 | this.pepper = pepper; 147 | return this; 148 | } 149 | 150 | /** 151 | * Hashes the previously given plain text password 152 | * with a specific implementation of {@link HashingFunction}. 153 | *

154 | * This method does not read the configurations in the `psw4j.properties` file. 155 | * 156 | * @param hashingFunction a CHF 157 | * @return a {@link Hash} object 158 | * @since 1.0.0 159 | */ 160 | public Hash with(HashingFunction hashingFunction) 161 | { 162 | return hashingFunction.hash(plainTextPassword, salt, pepper); 163 | } 164 | 165 | /** 166 | * Hashes the previously given plain text password 167 | * with {@link PBKDF2Function}. 168 | *

169 | * This method reads the configurations in the `psw4j.properties` file. If no configuration is found, 170 | * then the default parameters are used. 171 | *

172 | * Finally calls {@link #with(HashingFunction)} 173 | * 174 | * @return a {@link Hash} object 175 | * @see AlgorithmFinder#getPBKDF2Instance() 176 | * @see #with(HashingFunction) 177 | * @since 1.0.0 178 | */ 179 | public Hash withPBKDF2() 180 | { 181 | return with(AlgorithmFinder.getPBKDF2Instance()); 182 | } 183 | 184 | /** 185 | * Hashes the previously given plain text password 186 | * with {@link CompressedPBKDF2Function}. 187 | *

188 | * This method reads the configurations in the `psw4j.properties` file. If no configuration is found, 189 | * then the default parameters are used. 190 | *

191 | * Finally calls {@link #with(HashingFunction)} 192 | * 193 | * @return an {@link Hash} object 194 | * @see AlgorithmFinder#getCompressedPBKDF2Instance() 195 | * @see #with(HashingFunction) 196 | * @since 1.0.0 197 | */ 198 | public Hash withCompressedPBKDF2() 199 | { 200 | return with(AlgorithmFinder.getCompressedPBKDF2Instance()); 201 | } 202 | 203 | /** 204 | * Hashes the previously given plain text password 205 | * with {@link BcryptFunction}. 206 | *

207 | * This method reads the configurations in the `psw4j.properties` file. If no configuration is found, 208 | * then the default parameters are used. 209 | *

210 | * Finally calls {@link #with(HashingFunction)} 211 | * 212 | * @return an {@link Hash} object 213 | * @see AlgorithmFinder#getBcryptInstance() 214 | * @see #with(HashingFunction) 215 | * @since 1.0.0 216 | */ 217 | public Hash withBcrypt() 218 | { 219 | return with(AlgorithmFinder.getBcryptInstance()); 220 | } 221 | 222 | /** 223 | * Hashes the previously given plain text password 224 | * with {@link ScryptFunction}. 225 | *

226 | * This method reads the configurations in the `psw4j.properties` file. If no configuration is found, 227 | * then the default parameters are used. 228 | *

229 | * Finally calls {@link #with(HashingFunction)} 230 | * 231 | * @return an {@link Hash} object 232 | * @see AlgorithmFinder#getScryptInstance() 233 | * @see #with(HashingFunction) 234 | * @since 1.0.0 235 | */ 236 | public Hash withScrypt() 237 | { 238 | return with(AlgorithmFinder.getScryptInstance()); 239 | } 240 | 241 | /** 242 | * Hashes the previously given plain text password 243 | * with {@link MessageDigestFunction}. 244 | *

245 | * This method reads the configurations in the `psw4j.properties` file. If no configuration is found, 246 | * then the default parameters are used. 247 | *

248 | * Finally calls {@link #with(HashingFunction)} 249 | * 250 | * @return a {@link Hash} object 251 | * @see AlgorithmFinder#getPBKDF2Instance() 252 | * @see #with(HashingFunction) 253 | * @since 1.4.0 254 | */ 255 | public Hash withMessageDigest() 256 | { 257 | return with(AlgorithmFinder.getMessageDigestInstance()); 258 | } 259 | 260 | /** 261 | * Hashes the previously given plain text password 262 | * with {@link Argon2Function}. 263 | *

264 | * This method reads the configurations in the `psw4j.properties` file. If no configuration is found, 265 | * then the default parameters are used. 266 | *

267 | * Finally calls {@link #with(HashingFunction)} 268 | * 269 | * @return a {@link Hash} object 270 | * @see AlgorithmFinder#getArgon2Instance() 271 | * @see #with(HashingFunction) 272 | * @since 1.5.0 273 | */ 274 | public Hash withArgon2() 275 | { 276 | return with(AlgorithmFinder.getArgon2Instance()); 277 | } 278 | 279 | /** 280 | * Hashes the previously given plain text password 281 | * with {@link BalloonHashingFunction}. 282 | *

283 | * This method reads the configurations in the `psw4j.properties` file. If no configuration is found, 284 | * then the default parameters are used. 285 | *

286 | * Finally calls {@link #with(HashingFunction)} 287 | * 288 | * @return a {@link Hash} object 289 | * @see AlgorithmFinder#getArgon2Instance() 290 | * @see #with(HashingFunction) 291 | * @since 1.8.0 292 | */ 293 | public Hash withBalloonHashing() 294 | { 295 | return with(AlgorithmFinder.getBalloonHashingInstance()); 296 | } 297 | 298 | } 299 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/HashChecker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2020 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package com.password4j; 18 | 19 | /** 20 | * Builder class that helps to create a chain of parameters to be used 21 | * in the verification process. 22 | * 23 | * @author David Bertoldi 24 | * @since 1.0.0 25 | */ 26 | public class HashChecker 27 | { 28 | protected byte[] hashed; 29 | protected byte[] salt; 30 | private byte[] plainTextPassword; 31 | private CharSequence pepper; 32 | 33 | 34 | @SuppressWarnings("unused") 35 | private HashChecker() 36 | { 37 | // 38 | } 39 | 40 | /** 41 | * @param plainTextPassword the plain text password 42 | * @param hashed the hash to verify 43 | * @since 1.0.0 44 | */ 45 | HashChecker(CharSequence plainTextPassword, String hashed) 46 | { 47 | this.hashed = Utils.fromCharSequenceToBytes(hashed); 48 | this.plainTextPassword = Utils.fromCharSequenceToBytes(plainTextPassword); 49 | } 50 | 51 | /** 52 | * @param plainTextPassword the plain text password as bytes array 53 | * @param hashed the hash to verify as bytes array 54 | * @since 1.7.0 55 | */ 56 | HashChecker(byte[] plainTextPassword, byte[] hashed) 57 | { 58 | this.hashed = hashed; 59 | this.plainTextPassword = plainTextPassword; 60 | } 61 | 62 | /** 63 | * Concatenates the provided string with the plain text password. 64 | * The produced sequence (in the form {@code pepper+password}) is processed by the algorithm. 65 | * 66 | * @param pepper cryptographic pepper 67 | * @return this builder 68 | * @since 1.0.0 69 | */ 70 | public HashChecker addPepper(CharSequence pepper) 71 | { 72 | this.pepper = pepper; 73 | return this; 74 | } 75 | 76 | /** 77 | * Concatenates the pepper configured in your `psw4j.properties` file with the plain text password. 78 | * The produced sequence (in the form {@code pepper+password}) is processed by the algorithm. 79 | * 80 | * @return this builder 81 | * @see PepperGenerator#get() 82 | */ 83 | public HashChecker addPepper() 84 | { 85 | this.pepper = PepperGenerator.get(); 86 | return this; 87 | } 88 | 89 | /** 90 | * Add a cryptographic salt in the verifying process. 91 | * The salt is applied differently depending on the chosen algorithm. 92 | * 93 | * @param salt cryptographic salt 94 | * @return this builder 95 | * @since 1.0.0 96 | */ 97 | public HashChecker addSalt(String salt) 98 | { 99 | this.salt = Utils.fromCharSequenceToBytes(salt); 100 | return this; 101 | } 102 | 103 | /** 104 | * Add a cryptographic salt in the verifying process. 105 | * The salt is applied differently depending on the chosen algorithm. 106 | * 107 | * @param salt cryptographic salt as bytes array 108 | * @return this builder 109 | * @since 1.7.0 110 | */ 111 | public HashChecker addSalt(byte[] salt) 112 | { 113 | this.salt = salt; 114 | return this; 115 | } 116 | 117 | /** 118 | * Creates a builder to update the hash. 119 | * The actual salt and pepper are taken from the original check request. 120 | *

121 | * In order to declare a new salt or pepper use ,} 122 | * 123 | * @return the updater 124 | * @since 1.3.0 125 | */ 126 | public HashUpdater andUpdate() 127 | { 128 | return new HashUpdater(this, new HashBuilder(plainTextPassword).addPepper(pepper).addSalt(salt)); 129 | } 130 | 131 | /** 132 | * Check if the previously given hash was produced from the given plain text password 133 | * with a specific implementation of {@link HashingFunction}. 134 | *

135 | * This method does not read the configurations in the `psw4j.properties` file. 136 | * 137 | * @param hashingFunction a CHF 138 | * @return true if the hash was produced by the given plain text password; false otherwise. 139 | * @since 1.0.0 140 | */ 141 | public boolean with(HashingFunction hashingFunction) 142 | { 143 | if (plainTextPassword == null || plainTextPassword.length == 0) 144 | { 145 | return false; 146 | } 147 | 148 | return hashingFunction.check(plainTextPassword, hashed, salt, pepper); 149 | } 150 | 151 | /** 152 | * Check if the previously given hash was produced from the given plain text password 153 | * with {@link PBKDF2Function}. 154 | *

155 | * This method reads the configurations in the `psw4j.properties` file. If no configuration is found, 156 | * then the default parameters are used. 157 | * 158 | * @return true if the hash was produced by the given plain text password; false otherwise. 159 | * @see AlgorithmFinder#getPBKDF2Instance() 160 | * @since 1.0.0 161 | */ 162 | public boolean withPBKDF2() 163 | { 164 | PBKDF2Function pbkdf2 = AlgorithmFinder.getPBKDF2Instance(); 165 | return with(pbkdf2); 166 | } 167 | 168 | /** 169 | * Check if the previously given hash was produced from the given plain text password 170 | * with {@link CompressedPBKDF2Function}. 171 | *

172 | * This method reads the configurations in the `psw4j.properties` file. If no configuration is found, 173 | * then the default parameters are used. 174 | * 175 | * @return true if the hash was produced by the given plain text password; false otherwise. 176 | * @see AlgorithmFinder#getCompressedPBKDF2Instance() 177 | * @since 1.0.0 178 | */ 179 | public boolean withCompressedPBKDF2() 180 | { 181 | PBKDF2Function pbkdf2 = AlgorithmFinder.getCompressedPBKDF2Instance(); 182 | return with(pbkdf2); 183 | } 184 | 185 | /** 186 | * Check if the previously given hash was produced from the given plain text password 187 | * with {@link ScryptFunction}. 188 | *

189 | * This method reads the configurations in the `psw4j.properties` file. If no configuration is found, 190 | * then the default parameters are used. 191 | * 192 | * @return true if the hash was produced by the given plain text password; false otherwise. 193 | * @see AlgorithmFinder#getScryptInstance() 194 | * @since 1.0.0 195 | */ 196 | public boolean withScrypt() 197 | { 198 | ScryptFunction scrypt = AlgorithmFinder.getScryptInstance(); 199 | return with(scrypt); 200 | } 201 | 202 | /** 203 | * Check if the previously given hash was produced from the given plain text password 204 | * with {@link BcryptFunction}. 205 | *

206 | * This method reads the configurations in the `psw4j.properties` file. If no configuration is found, 207 | * then the default parameters are used. 208 | * 209 | * @return true if the hash was produced by the given plain text password; false otherwise. 210 | * @see AlgorithmFinder#getBcryptInstance() 211 | * @since 1.0.0 212 | */ 213 | public boolean withBcrypt() 214 | { 215 | return with(AlgorithmFinder.getBcryptInstance()); 216 | } 217 | 218 | /** 219 | * Check if the previously given hash was produced from the given plain text password 220 | * with {@link MessageDigestFunction}. 221 | *

222 | * This method reads the configurations in the `psw4j.properties` file. If no configuration is found, 223 | * then the default parameters are used. 224 | * 225 | * @return true if the hash was produced by the given plain text password; false otherwise. 226 | * @see AlgorithmFinder#getMessageDigestInstance() 227 | * @since 1.4.0 228 | */ 229 | public boolean withMessageDigest() 230 | { 231 | return with(AlgorithmFinder.getMessageDigestInstance()); 232 | } 233 | 234 | /** 235 | * Check if the previously given hash was produced from the given plain text password 236 | * with {@link Argon2Function}. 237 | *

238 | * This method reads the configurations in the `psw4j.properties` file. If no configuration is found, 239 | * then the default parameters are used. 240 | * 241 | * @return true if the hash was produced by the given plain text password; false otherwise. 242 | * @see AlgorithmFinder#getArgon2Instance() 243 | * @since 1.5.0 244 | */ 245 | public boolean withArgon2() 246 | { 247 | Argon2Function argon2 = AlgorithmFinder.getArgon2Instance(); 248 | return with(argon2); 249 | } 250 | 251 | /** 252 | * Check if the previously given hash was produced from the given plain text password 253 | * with {@link BalloonHashingFunction}. 254 | *

255 | * This method reads the configurations in the `psw4j.properties` file. If no configuration is found, 256 | * then the default parameters are used. 257 | * 258 | * @return true if the hash was produced by the given plain text password; false otherwise. 259 | * @see AlgorithmFinder#getBalloonHashingInstance() 260 | * @since 1.5.0 261 | */ 262 | public boolean withBalloonHashing() 263 | { 264 | BalloonHashingFunction balloon = AlgorithmFinder.getBalloonHashingInstance(); 265 | return with(balloon); 266 | } 267 | 268 | /** 269 | * This method returns the String version of the hash bytes. This 270 | * should be always a safe operation when using ISO-8859-1 encoding. 271 | * 272 | * @return String version of the hash 273 | */ 274 | protected String getHashed() 275 | { 276 | return Utils.fromBytesToString(hashed); 277 | } 278 | 279 | } 280 | -------------------------------------------------------------------------------- /src/main/java/com/password4j/BalloonHashingFunction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2023 Password4j (http://password4j.com/). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.password4j; 19 | 20 | import java.math.BigInteger; 21 | import java.security.MessageDigest; 22 | import java.security.NoSuchAlgorithmException; 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | import java.util.Map; 26 | import java.util.Objects; 27 | import java.util.concurrent.ConcurrentHashMap; 28 | import java.util.concurrent.ExecutionException; 29 | import java.util.concurrent.ExecutorService; 30 | import java.util.concurrent.Future; 31 | 32 | /** 33 | * Class containing the implementation of Balloon hashing function and its parameters. 34 | * 35 | * @author David Bertoldi 36 | * @since 1.8.0 37 | */ 38 | public class BalloonHashingFunction extends AbstractHashingFunction 39 | { 40 | 41 | private static final Map INSTANCES = new ConcurrentHashMap<>(); 42 | private static final int DEFAULT_DELTA = 3; 43 | 44 | private final String algorithm; 45 | private final int spaceCost; 46 | private final int timeCost; 47 | private final int parallelism; 48 | private final int delta; 49 | private ExecutorService service; 50 | 51 | BalloonHashingFunction(String algorithm, int spaceCost, int timeCost, int parallelism, int delta) 52 | { 53 | this.algorithm = algorithm; 54 | this.spaceCost = spaceCost; 55 | this.timeCost = timeCost; 56 | this.parallelism = parallelism; 57 | this.delta = delta; 58 | if (parallelism > 1) 59 | { 60 | this.service = Utils.createExecutorService(); 61 | } 62 | 63 | } 64 | 65 | 66 | public static BalloonHashingFunction getInstance(String algorithm, int spaceCost, int timeCost, int parallelism, int delta) 67 | { 68 | String key = getUID(algorithm, spaceCost, timeCost, parallelism, delta); 69 | if (INSTANCES.containsKey(key)) 70 | { 71 | return INSTANCES.get(key); 72 | } 73 | else 74 | { 75 | BalloonHashingFunction function = new BalloonHashingFunction(algorithm, spaceCost, timeCost, parallelism, delta); 76 | INSTANCES.put(key, function); 77 | return function; 78 | } 79 | } 80 | 81 | public static BalloonHashingFunction getInstance(String algorithm, int spaceCost, int timeCost, int parallelism) 82 | { 83 | return getInstance(algorithm, spaceCost, timeCost, parallelism, DEFAULT_DELTA); 84 | } 85 | 86 | private static String getUID(String algorithm, int spaceCost, int timeCost, int parallelism, int delta) 87 | { 88 | return algorithm + '|' + spaceCost + '|' + timeCost + '|' + parallelism + '|' + delta; 89 | } 90 | 91 | protected static String toString(String algorithm, int spaceCost, int timeCost, int parallelism, int delta) 92 | { 93 | return "a=" + algorithm + ", s=" + spaceCost + ", t=" + timeCost + ", p=" + parallelism + ", d=" + delta; 94 | } 95 | 96 | @Override 97 | public Hash hash(CharSequence plainTextPassword) 98 | { 99 | return null; 100 | } 101 | 102 | @Override 103 | public Hash hash(byte[] plainTextPassword) 104 | { 105 | return null; 106 | } 107 | 108 | @Override 109 | public Hash hash(CharSequence plainTextPassword, String salt) 110 | { 111 | return hash(Utils.fromCharSequenceToBytes(plainTextPassword), Utils.fromCharSequenceToBytes(salt)); 112 | } 113 | 114 | @Override 115 | public Hash hash(byte[] plainTextPassword, byte[] salt) 116 | { 117 | return internalHash(plainTextPassword, salt); 118 | } 119 | 120 | protected Hash internalHash(byte[] plainTextPassword, byte[] salt) 121 | { 122 | 123 | byte[] output; 124 | 125 | if (parallelism == 1) 126 | { 127 | byte[] parallelSalt = Utils.append(salt, Utils.longToLittleEndian((1))); 128 | MessageDigest messageDigest = getMessageDigest(); 129 | output = balloon(messageDigest, plainTextPassword, parallelSalt); 130 | output = hashFunc(messageDigest, plainTextPassword, salt, output); 131 | } 132 | else if (parallelism > 1) 133 | { 134 | 135 | List> futures = new ArrayList<>(); 136 | 137 | for (int i = 0; i < parallelism; i++) 138 | { 139 | byte[] parallelSalt = Utils.append(salt, Utils.longToLittleEndian((i + 1))); 140 | Future future = service.submit(() -> balloon(getMessageDigest(), plainTextPassword, parallelSalt)); 141 | 142 | futures.add(future); 143 | } 144 | 145 | MessageDigest messageDigest = getMessageDigest(); 146 | output = new byte[messageDigest.getDigestLength()]; 147 | 148 | try 149 | { 150 | byte[] tmp; 151 | output = (byte[]) futures.get(0).get(); 152 | for (int f = 1; f < futures.size(); f++) 153 | { 154 | tmp = ((byte[]) futures.get(f).get()); 155 | 156 | for (int i = 0; i < output.length; i++) 157 | { 158 | output[i] ^= tmp[i]; 159 | } 160 | 161 | } 162 | } 163 | catch (InterruptedException | ExecutionException e) 164 | { 165 | Thread.currentThread().interrupt(); 166 | } 167 | 168 | 169 | output = hashFunc(messageDigest, plainTextPassword, salt, output); 170 | } 171 | else 172 | { 173 | output = balloon(getMessageDigest(), plainTextPassword, salt); 174 | } 175 | 176 | return new Hash(this, Utils.toHex(output), output, salt); 177 | } 178 | 179 | protected MessageDigest getMessageDigest() 180 | { 181 | try 182 | { 183 | return MessageDigest.getInstance(algorithm); 184 | } 185 | catch (NoSuchAlgorithmException nsae) 186 | { 187 | throw new UnsupportedOperationException("`" + algorithm + "` is not supported by your system.", nsae); 188 | } 189 | } 190 | 191 | private byte[] balloon(MessageDigest messageDigest, byte[] plainTextPassword, byte[] salt) 192 | { 193 | List buffer = new ArrayList<>(); 194 | buffer.add(hashFunc(messageDigest, 0, plainTextPassword, salt)); 195 | 196 | int cnt = 1; 197 | 198 | cnt = expand(messageDigest, buffer, cnt); 199 | mix(messageDigest, buffer, cnt, salt); 200 | return extract(buffer); 201 | } 202 | 203 | private int expand(MessageDigest messageDigest, List buffer, int cnt) 204 | { 205 | int newCnt = cnt; 206 | for (int i = 1; i < spaceCost; i++) 207 | { 208 | buffer.add(hashFunc(messageDigest, newCnt, buffer.get(i - 1))); 209 | newCnt += 1; 210 | } 211 | return newCnt; 212 | } 213 | 214 | private void mix(MessageDigest messageDigest, List buffer, int cnt, byte[] salt) 215 | { 216 | int newCnt = cnt; 217 | for (int t = 0; t < timeCost; t++) 218 | { 219 | for (int s = 0; s < spaceCost; s++) 220 | { 221 | buffer.set(s, hashFunc(messageDigest, newCnt, get(buffer, s - 1), get(buffer, s))); 222 | newCnt += 1; 223 | 224 | for (int d = 0; d < delta; d++) 225 | { 226 | byte[] indexBlock = hashFunc(messageDigest, t, s, d); 227 | int other = Utils.bytesToInt(hashFunc(messageDigest, newCnt, salt, indexBlock)).mod(BigInteger.valueOf(spaceCost)).intValue(); 228 | newCnt += 1; 229 | buffer.set(s, hashFunc(messageDigest, newCnt, buffer.get(s), get(buffer, other))); 230 | newCnt += 1; 231 | } 232 | } 233 | } 234 | } 235 | 236 | private byte[] extract(List buffer) 237 | { 238 | return buffer.get(buffer.size() - 1); 239 | } 240 | 241 | private byte[] get(List buffer, int position) 242 | { 243 | if (position < 0) 244 | { 245 | return buffer.get(buffer.size() + position); 246 | } 247 | return buffer.get(position); 248 | } 249 | 250 | private byte[] hashFunc(MessageDigest md, Object... args) 251 | { 252 | byte[] t = new byte[0]; 253 | 254 | for (Object arg : args) 255 | { 256 | if (arg instanceof Integer) 257 | { 258 | t = Utils.append(t, Utils.intToLittleEndianBytes((Integer) arg, 8)); 259 | } 260 | else if (arg instanceof CharSequence) 261 | { 262 | t = Utils.append(t, Utils.fromCharSequenceToBytes((CharSequence) arg)); 263 | } 264 | else if (arg instanceof byte[]) 265 | { 266 | t = Utils.append(t, (byte[]) arg); 267 | } 268 | } 269 | 270 | return md.digest(t); 271 | } 272 | 273 | @Override 274 | public boolean check(CharSequence plainTextPassword, String hashed) 275 | { 276 | return check(plainTextPassword, hashed, null); 277 | } 278 | 279 | @Override 280 | public boolean check(CharSequence plainTextPassword, String hashed, String salt) 281 | { 282 | Hash hash = internalHash(Utils.fromCharSequenceToBytes(plainTextPassword), Utils.fromCharSequenceToBytes(salt)); 283 | return slowEquals(hash.getResult(), hashed); 284 | } 285 | 286 | @Override 287 | public boolean check(byte[] plainTextPassword, byte[] hashed) 288 | { 289 | return check(plainTextPassword, new byte[0], hashed, null); 290 | } 291 | 292 | @Override 293 | public boolean check(byte[] plainTextPassword, byte[] hashed, byte[] salt) 294 | { 295 | Hash hash = internalHash(plainTextPassword, salt); 296 | return slowEquals(hash.getResultAsBytes(), hashed); 297 | } 298 | 299 | @Override 300 | public boolean equals(Object o) 301 | { 302 | if (this == o) 303 | return true; 304 | if (!(o instanceof BalloonHashingFunction)) 305 | return false; 306 | BalloonHashingFunction other = (BalloonHashingFunction) o; 307 | return algorithm.equals(other.algorithm) // 308 | && spaceCost == other.spaceCost // 309 | && timeCost == other.timeCost // 310 | && parallelism == other.parallelism // 311 | && delta == other.delta; 312 | } 313 | 314 | @Override 315 | public int hashCode() 316 | { 317 | return Objects.hash(algorithm, spaceCost, timeCost, parallelism, delta); 318 | } 319 | 320 | @Override 321 | public String toString() 322 | { 323 | return getClass().getSimpleName() + '[' + toString(algorithm, spaceCost, timeCost, parallelism, delta) + ']'; 324 | } 325 | } 326 | --------------------------------------------------------------------------------