From 1e15994326f6dc901c3b562966e84664fce668e0 Mon Sep 17 00:00:00 2001 From: Benjamin Marwell Date: Thu, 31 Dec 2020 10:45:35 +0100 Subject: [PATCH] [SHIRO-290] Implement BCrypt and Argon2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [SHIRO-290] WIP: Implement Unix crypt format, starting with bcrypt. - TBD: HashRequest - TBD: PasswortMatcher doesn’t know about the new hash format yet [SHIRO-290] Rework to use existing Shiro1CryptFormat. - Hashes can now compare themselves to a given password. Reviwers: Review method placement and HAsh class description. - removed hashrequest - removed UnixCryptFormat - API change: made salt not-nullable. Additional constructor is supplied for hashing without or with default salt, the former and other methods/fields using SimpleByteSource.empty(). Reviewers: Pay attention to method logic, so no empty salt is being used where a former `null` value would have created a new, random salt. - Modified tests to not expect exceptions in certain cases. - Modified tests to not expect passwordService calls when supplying an existing hash. - TBD: Fix Javadocs - TBD: Fix Hasher utility - TBD: Deprecate old non-KDF hash classes [SHIRO-290] Prepare argon2 implementation. - BCrypt iterations vs cost: make iterations return iterations - add validate methods [SHIRO-290] Implement Argon2Hash.java. - expand iterations field to take a comma separated list. Maybe just create a Shiro2CryptFormat instead? - Hex and Base64 formats are not fixed. Maybe we can drop them? - Fixed parameter "algorithm name" not taken into account for bcrypt. - Allow Hasher to read from stdin - Added a short test for Hasher.java. - Changed default DefaultPasswordService.java algorithm to "Argon2id". [SHIRO-290] Implement Shiro2CryptFormat.java. - Only fields 1 and two are defined, rest is defined by the hash implementation - Therefore fully backwards-compatible to Shiro1CryptFormat.java. - Loads formats from ProvidedKdfHashes.java. We could also think of a pluggable mechanism, like using service loaders to hide classes like OpenBSDBase64. - In AbstractCryptHash.java, renamed `version` to `algorithmName`. - Removed iterations from AbstractCryptHash.java, they are possibly an implementation detail not present in other implementations (like bcrypt). - Signature change: `PasswordService.encryptPassword(Object plaintext)` will now throw a NullPointerException on `null` parameter. It was never specified how this method would behave. [SHIRO-290] Add hasher tests - fix invalid cost factor for bcrypt when input is 0. - output Hasher messages using slf4j. [SHIRO-290] ServiceLoadable KDF algorithms. - Move BCrypt and Argon2 into their own modules - Add a SPI - Remove hardcoded parameters, replace with ParameterMap for the hashRequest [SHIRO-290] implemented review comments - remove at least MD2, MD5 and Sha1 - Remove unused support-hashes module - changed group and artifact-ids for new modules - fixed compilation issue in Hasher (needs more work though) - add "since 2.0" comments [SHIRO-290] add some javadoc, make implementation classes package-private. [SHIRO-290] doc updates --- RELEASE-NOTES | 20 +- core/pom.xml | 10 + .../shiro/authc/SimpleAuthenticationInfo.java | 26 +- .../credential/DefaultPasswordService.java | 58 +-- .../credential/HashedCredentialsMatcher.java | 32 +- .../credential/Md2CredentialsMatcher.java | 47 --- .../credential/Md5CredentialsMatcher.java | 46 --- .../authc/credential/PasswordMatcher.java | 18 +- .../credential/Sha1CredentialsMatcher.java | 46 --- .../realm/text/TextConfigurationRealm.java | 1 + .../DefaultPasswordServiceTest.groovy | 49 ++- .../credential/PasswordMatcherTest.groovy | 49 ++- .../HashedCredentialsMatcherTest.java | 24 +- .../credential/Md2CredentialsMatcherTest.java | 39 -- .../credential/Md5CredentialsMatcherTest.java | 37 -- .../Sha1CredentialsMatcherTest.java | 37 -- crypto/cipher/pom.xml | 1 - crypto/hash/pom.xml | 5 + .../shiro/crypto/hash/AbstractCryptHash.java | 253 +++++++++++++ .../shiro/crypto/hash/AbstractHash.java | 74 +--- .../crypto/hash/ConfigurableHashService.java | 34 +- .../shiro/crypto/hash/DefaultHashService.java | 281 ++------------ .../org/apache/shiro/crypto/hash/Hash.java | 12 +- .../shiro/crypto/hash/HashProvider.java | 62 ++++ .../apache/shiro/crypto/hash/HashRequest.java | 75 ++-- .../org/apache/shiro/crypto/hash/HashSpi.java | 87 +++++ .../org/apache/shiro/crypto/hash/Md2Hash.java | 65 ---- .../org/apache/shiro/crypto/hash/Md5Hash.java | 66 ---- .../apache/shiro/crypto/hash/Sha1Hash.java | 67 ---- .../apache/shiro/crypto/hash/SimpleHash.java | 85 ++++- .../shiro/crypto/hash/SimpleHashProvider.java | 219 +++++++++++ .../shiro/crypto/hash/SimpleHashRequest.java | 51 +-- .../crypto/hash/format/Base64Format.java | 14 +- .../hash/format/DefaultHashFormatFactory.java | 5 +- .../shiro/crypto/hash/format/HashFormat.java | 5 +- .../shiro/crypto/hash/format/HexFormat.java | 12 +- .../hash/format/ProvidedHashFormat.java | 13 +- .../crypto/hash/format/Shiro1CryptFormat.java | 18 +- .../crypto/hash/format/Shiro2CryptFormat.java | 143 +++++++ .../hash/src/main/resources/META-INF/NOTICE | 2 +- .../org.apache.shiro.crypto.hash.HashSpi | 20 + .../crypto/hash/DefaultHashServiceTest.groovy | 82 +--- .../crypto/hash/HashRequestBuilderTest.groovy | 31 +- .../hash/format/Base64FormatTest.groovy | 10 +- .../DefaultHashFormatFactoryTest.groovy | 7 +- .../crypto/hash/format/HexFormatTest.groovy | 12 +- .../hash/format/ProvidedHashFormatTest.groovy | 3 +- .../hash/format/Shiro1CryptFormatTest.groovy | 7 +- crypto/pom.xml | 1 + crypto/support/hashes/argon2/pom.xml | 79 ++++ .../support/hashes/argon2/Argon2Hash.java | 349 ++++++++++++++++++ .../hashes/argon2/Argon2HashProvider.java | 207 +++++++++++ .../argon2/src/main/resources/META-INF/NOTICE | 18 + .../org.apache.shiro.crypto.hash.HashSpi | 20 + .../hashes/argon2/Argon2HashTest.groovy | 88 +++++ crypto/support/hashes/bcrypt/pom.xml | 79 ++++ .../support/hashes/bcrypt/BCryptHash.java | 200 ++++++++++ .../support/hashes/bcrypt/BCryptProvider.java | 144 ++++++++ .../support/hashes/bcrypt/OpenBSDBase64.java | 179 +++++++++ .../bcrypt/src/main/resources/META-INF/NOTICE | 18 + .../org.apache.shiro.crypto.hash.HashSpi | 20 + .../hashes/bcrypt/BCryptHashTest.groovy | 98 +++++ crypto/support/pom.xml | 44 +++ .../apache/shiro/lang/codec/CodecSupport.java | 40 +- .../shiro/lang/util/SimpleByteSource.java | 13 +- .../apache/shiro/lang/util/StringUtils.java | 12 + pom.xml | 17 + tools/hasher/pom.xml | 17 +- .../org/apache/shiro/tools/hasher/Hasher.java | 70 ++-- tools/hasher/src/main/resources/logback.xml | 31 ++ .../apache/shiro/tools/hasher/HasherTest.java | 101 +++++ .../src/test/resources/logback-test.xml | 28 ++ 72 files changed, 3077 insertions(+), 1156 deletions(-) delete mode 100644 core/src/main/java/org/apache/shiro/authc/credential/Md2CredentialsMatcher.java delete mode 100644 core/src/main/java/org/apache/shiro/authc/credential/Md5CredentialsMatcher.java delete mode 100644 core/src/main/java/org/apache/shiro/authc/credential/Sha1CredentialsMatcher.java delete mode 100644 core/src/test/java/org/apache/shiro/authc/credential/Md2CredentialsMatcherTest.java delete mode 100644 core/src/test/java/org/apache/shiro/authc/credential/Md5CredentialsMatcherTest.java delete mode 100644 core/src/test/java/org/apache/shiro/authc/credential/Sha1CredentialsMatcherTest.java create mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/AbstractCryptHash.java create mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashProvider.java create mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashSpi.java delete mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Md2Hash.java delete mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Md5Hash.java delete mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Sha1Hash.java create mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashProvider.java create mode 100644 crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Shiro2CryptFormat.java create mode 100644 crypto/hash/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi create mode 100644 crypto/support/hashes/argon2/pom.xml create mode 100644 crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2Hash.java create mode 100644 crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashProvider.java create mode 100644 crypto/support/hashes/argon2/src/main/resources/META-INF/NOTICE create mode 100644 crypto/support/hashes/argon2/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi create mode 100644 crypto/support/hashes/argon2/src/test/groovy/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashTest.groovy create mode 100644 crypto/support/hashes/bcrypt/pom.xml create mode 100644 crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHash.java create mode 100644 crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptProvider.java create mode 100644 crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/OpenBSDBase64.java create mode 100644 crypto/support/hashes/bcrypt/src/main/resources/META-INF/NOTICE create mode 100644 crypto/support/hashes/bcrypt/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi create mode 100644 crypto/support/hashes/bcrypt/src/test/groovy/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHashTest.groovy create mode 100644 crypto/support/pom.xml create mode 100644 tools/hasher/src/main/resources/logback.xml create mode 100644 tools/hasher/src/test/java/org/apache/shiro/tools/hasher/HasherTest.java create mode 100644 tools/hasher/src/test/resources/logback-test.xml diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 7b39af3b5d..42bf34243f 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -17,9 +17,27 @@ This is not an official release notes document. It exists for Shiro developers to jot down their notes while working in the source code. These notes will be -combined with Jira's auto-generated release notes during a release for the +combined with Jira’s auto-generated release notes during a release for the total set. +########################################################### +# 2.0.0 +########################################################### + +Improvement + + [SHIRO-290] Implement bcrypt and argon2 KDF algorithms + +Backwards Incompatible Changes +-------------------------------- + +* Changed default DefaultPasswordService.java algorithm to "Argon2id". +* PasswordService.encryptPassword(Object plaintext) will now throw a NullPointerException on null parameter. + It was never specified how this method would behave. +* Made salt non-nullable. +* Removed methods in PasswordMatcher. + + ########################################################### # 1.5.3 ########################################################### diff --git a/core/pom.xml b/core/pom.xml index 3c247f1b0a..5de40a9f81 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -101,6 +101,16 @@ org.apache.shiro shiro-crypto-hash + + org.apache.shiro.crypto + shiro-hashes-argon2 + runtime + + + org.apache.shiro.crypto + shiro-hashes-bcrypt + runtime + org.apache.shiro shiro-crypto-cipher diff --git a/core/src/main/java/org/apache/shiro/authc/SimpleAuthenticationInfo.java b/core/src/main/java/org/apache/shiro/authc/SimpleAuthenticationInfo.java index 63d3cf5b8e..612d4a904b 100644 --- a/core/src/main/java/org/apache/shiro/authc/SimpleAuthenticationInfo.java +++ b/core/src/main/java/org/apache/shiro/authc/SimpleAuthenticationInfo.java @@ -18,13 +18,15 @@ */ package org.apache.shiro.authc; +import org.apache.shiro.lang.util.ByteSource; +import org.apache.shiro.lang.util.SimpleByteSource; import org.apache.shiro.subject.MutablePrincipalCollection; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.SimplePrincipalCollection; -import org.apache.shiro.lang.util.ByteSource; import java.util.Collection; import java.util.HashSet; +import java.util.Objects; import java.util.Set; @@ -37,6 +39,7 @@ */ public class SimpleAuthenticationInfo implements MergableAuthenticationInfo, SaltedAuthenticationInfo { + private static final long serialVersionUID = 5390456512469696779L; /** * The principals identifying the account associated with this AuthenticationInfo instance. */ @@ -51,7 +54,7 @@ public class SimpleAuthenticationInfo implements MergableAuthenticationInfo, Sal * * @since 1.1 */ - protected ByteSource credentialsSalt; + protected ByteSource credentialsSalt = SimpleByteSource.empty(); /** * Default no-argument constructor. @@ -124,6 +127,7 @@ public SimpleAuthenticationInfo(PrincipalCollection principals, Object hashedCre } + @Override public PrincipalCollection getPrincipals() { return principals; } @@ -137,6 +141,7 @@ public void setPrincipals(PrincipalCollection principals) { this.principals = principals; } + @Override public Object getCredentials() { return credentials; } @@ -163,6 +168,7 @@ public void setCredentials(Object credentials) { * hashed at all. * @since 1.1 */ + @Override public ByteSource getCredentialsSalt() { return credentialsSalt; } @@ -189,6 +195,7 @@ public void setCredentialsSalt(ByteSource salt) { * * @param info the AuthenticationInfo to add into this instance. */ + @Override @SuppressWarnings("unchecked") public void merge(AuthenticationInfo info) { if (info == null || info.getPrincipals() == null || info.getPrincipals().isEmpty()) { @@ -249,14 +256,21 @@ public void merge(AuthenticationInfo info) { * @return true if the Object argument is an instanceof SimpleAuthenticationInfo and * its {@link #getPrincipals() principals} are equal to this instance's principals, false otherwise. */ + @Override public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof SimpleAuthenticationInfo)) return false; + if (this == o) { + return true; + } + if (!(o instanceof SimpleAuthenticationInfo)) { + return false; + } SimpleAuthenticationInfo that = (SimpleAuthenticationInfo) o; //noinspection RedundantIfStatement - if (principals != null ? !principals.equals(that.principals) : that.principals != null) return false; + if (!Objects.equals(principals, that.principals)) { + return false; + } return true; } @@ -266,6 +280,7 @@ public boolean equals(Object o) { * * @return the hashcode of the internal {@link #getPrincipals() principals} instance. */ + @Override public int hashCode() { return (principals != null ? principals.hashCode() : 0); } @@ -275,6 +290,7 @@ public int hashCode() { * * @return {@link #getPrincipals() principals}.toString() */ + @Override public String toString() { return principals.toString(); } diff --git a/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java b/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java index ea12668e4f..6c0578f16e 100644 --- a/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java +++ b/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java @@ -18,32 +18,36 @@ */ package org.apache.shiro.authc.credential; -import java.security.MessageDigest; - import org.apache.shiro.crypto.hash.DefaultHashService; import org.apache.shiro.crypto.hash.Hash; import org.apache.shiro.crypto.hash.HashRequest; import org.apache.shiro.crypto.hash.HashService; -import org.apache.shiro.crypto.hash.format.*; +import org.apache.shiro.crypto.hash.format.DefaultHashFormatFactory; +import org.apache.shiro.crypto.hash.format.HashFormat; +import org.apache.shiro.crypto.hash.format.HashFormatFactory; +import org.apache.shiro.crypto.hash.format.ParsableHashFormat; +import org.apache.shiro.crypto.hash.format.Shiro2CryptFormat; import org.apache.shiro.lang.util.ByteSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.security.MessageDigest; + +import static java.util.Objects.requireNonNull; + /** * Default implementation of the {@link PasswordService} interface that relies on an internal * {@link HashService}, {@link HashFormat}, and {@link HashFormatFactory} to function: *

Hashing Passwords

* *

Comparing Passwords

- * All hashing operations are performed by the internal {@link #getHashService() hashService}. After the hash - * is computed, it is formatted into a String value via the internal {@link #getHashFormat() hashFormat}. + * All hashing operations are performed by the internal {@link #getHashService() hashService}. * * @since 1.2 */ public class DefaultPasswordService implements HashingPasswordService { - public static final String DEFAULT_HASH_ALGORITHM = "SHA-256"; - public static final int DEFAULT_HASH_ITERATIONS = 500000; //500,000 + public static final String DEFAULT_HASH_ALGORITHM = "argon2id"; private static final Logger log = LoggerFactory.getLogger(DefaultPasswordService.class); @@ -53,25 +57,33 @@ public class DefaultPasswordService implements HashingPasswordService { private volatile boolean hashFormatWarned; //used to avoid excessive log noise + /** + * Constructs a new PasswordService with a default hash service and the default + * algorithm name {@value #DEFAULT_HASH_ALGORITHM}, a default hash format (shiro2) and + * a default hashformat factory. + * + *

The default algorithm can change between minor versions and does not introduce + * API incompatibility by design.

+ */ public DefaultPasswordService() { this.hashFormatWarned = false; DefaultHashService hashService = new DefaultHashService(); - hashService.setHashAlgorithmName(DEFAULT_HASH_ALGORITHM); - hashService.setHashIterations(DEFAULT_HASH_ITERATIONS); - hashService.setGeneratePublicSalt(true); //always want generated salts for user passwords to be most secure + hashService.setDefaultAlgorithmName(DEFAULT_HASH_ALGORITHM); this.hashService = hashService; - this.hashFormat = new Shiro1CryptFormat(); + this.hashFormat = new Shiro2CryptFormat(); this.hashFormatFactory = new DefaultHashFormatFactory(); } + @Override public String encryptPassword(Object plaintext) { - Hash hash = hashPassword(plaintext); + Hash hash = hashPassword(requireNonNull(plaintext)); checkHashFormatDurability(); return this.hashFormat.format(hash); } + @Override public Hash hashPassword(Object plaintext) { ByteSource plaintextBytes = createByteSource(plaintext); if (plaintextBytes == null || plaintextBytes.isEmpty()) { @@ -81,6 +93,7 @@ public Hash hashPassword(Object plaintext) { return hashService.computeHash(request); } + @Override public boolean passwordsMatch(Object plaintext, Hash saved) { ByteSource plaintextBytes = createByteSource(plaintext); @@ -92,11 +105,7 @@ public boolean passwordsMatch(Object plaintext, Hash saved) { } } - HashRequest request = buildHashRequest(plaintextBytes, saved); - - Hash computed = this.hashService.computeHash(request); - - return constantEquals(saved.toString(), computed.toString()); + return saved.matchesPassword(plaintextBytes); } private boolean constantEquals(String savedHash, String computedHash) { @@ -133,6 +142,7 @@ protected ByteSource createByteSource(Object o) { return ByteSource.Util.bytes(o); } + @Override public boolean passwordsMatch(Object submittedPlaintext, String saved) { ByteSource plaintextBytes = createByteSource(submittedPlaintext); @@ -151,9 +161,9 @@ public boolean passwordsMatch(Object submittedPlaintext, String saved) { //configuration changes. HashFormat discoveredFormat = this.hashFormatFactory.getInstance(saved); - if (discoveredFormat != null && discoveredFormat instanceof ParsableHashFormat) { + if (discoveredFormat instanceof ParsableHashFormat) { - ParsableHashFormat parsableHashFormat = (ParsableHashFormat)discoveredFormat; + ParsableHashFormat parsableHashFormat = (ParsableHashFormat) discoveredFormat; Hash savedHash = parsableHashFormat.parse(saved); return passwordsMatch(submittedPlaintext, savedHash); @@ -174,16 +184,6 @@ public boolean passwordsMatch(Object submittedPlaintext, String saved) { return constantEquals(saved, formatted); } - protected HashRequest buildHashRequest(ByteSource plaintext, Hash saved) { - //keep everything from the saved hash except for the source: - return new HashRequest.Builder().setSource(plaintext) - //now use the existing saved data: - .setAlgorithmName(saved.getAlgorithmName()) - .setSalt(saved.getSalt()) - .setIterations(saved.getIterations()) - .build(); - } - public HashService getHashService() { return hashService; } diff --git a/core/src/main/java/org/apache/shiro/authc/credential/HashedCredentialsMatcher.java b/core/src/main/java/org/apache/shiro/authc/credential/HashedCredentialsMatcher.java index 1377374b23..5e6b8ad73e 100644 --- a/core/src/main/java/org/apache/shiro/authc/credential/HashedCredentialsMatcher.java +++ b/core/src/main/java/org/apache/shiro/authc/credential/HashedCredentialsMatcher.java @@ -21,13 +21,16 @@ import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SaltedAuthenticationInfo; -import org.apache.shiro.lang.codec.Base64; -import org.apache.shiro.lang.codec.Hex; import org.apache.shiro.crypto.hash.AbstractHash; import org.apache.shiro.crypto.hash.Hash; import org.apache.shiro.crypto.hash.SimpleHash; +import org.apache.shiro.lang.codec.Base64; +import org.apache.shiro.lang.codec.Hex; +import org.apache.shiro.lang.util.SimpleByteSource; import org.apache.shiro.lang.util.StringUtils; +import static java.util.Objects.requireNonNull; + /** * A {@code HashedCredentialMatcher} provides support for hashing of supplied {@code AuthenticationToken} credentials * before being compared to those in the {@code AuthenticationInfo} from the data store. @@ -49,10 +52,7 @@ * and multiple hash iterations. Please read this excellent * Hashing Java article to learn about * salting and multiple iterations and why you might want to use them. (Note of sections 5 - * "Why add salt?" and 6 "Hardening against the attacker's attack"). We should also note here that all of - * Shiro's Hash implementations (for example, {@link org.apache.shiro.crypto.hash.Md5Hash Md5Hash}, - * {@link org.apache.shiro.crypto.hash.Sha1Hash Sha1Hash}, etc) support salting and multiple hash iterations via - * overloaded constructors. + * "Why add salt?" and 6 "Hardening against the attacker's attack").

*

Real World Case Study

* In April 2010, some public Atlassian Jira and Confluence * installations (Apache Software Foundation, Codehaus, etc) were the target of account attacks and user accounts @@ -112,8 +112,8 @@ * two, if your application mandates high security, use the SHA-256 (or higher) hashing algorithms and their * supporting {@code CredentialsMatcher} implementations. * - * @see org.apache.shiro.crypto.hash.Md5Hash - * @see org.apache.shiro.crypto.hash.Sha1Hash + * @see org.apache.shiro.crypto.hash.Sha256Hash + * @see org.apache.shiro.crypto.hash.Sha384Hash * @see org.apache.shiro.crypto.hash.Sha256Hash * @since 0.9 */ @@ -341,6 +341,7 @@ protected Object getSalt(AuthenticationToken token) { * @param info the AuthenticationInfo from which to retrieve the credentials which assumed to be in already-hashed form. * @return a {@link Hash Hash} instance representing the given AuthenticationInfo's stored credentials. */ + @Override protected Object getCredentials(AuthenticationInfo info) { Object credentials = info.getCredentials(); @@ -400,14 +401,14 @@ public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo * @since 1.1 */ protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) { - Object salt = null; + final Object salt; if (info instanceof SaltedAuthenticationInfo) { salt = ((SaltedAuthenticationInfo) info).getCredentialsSalt(); - } else { + } else if (isHashSalted()) { //retain 1.0 backwards compatibility: - if (isHashSalted()) { - salt = getSalt(token); - } + salt = getSalt(token); + } else { + salt = SimpleByteSource.empty(); } return hashProvidedCredentials(token.getCredentials(), salt, getHashIterations()); } @@ -435,14 +436,15 @@ private String assertHashAlgorithmName() throws IllegalStateException { * implementation/algorithm used is based on the {@link #getHashAlgorithmName() hashAlgorithmName} property. * * @param credentials the submitted authentication token's credentials to hash - * @param salt the value to salt the hash, or {@code null} if a salt will not be used. + * @param salt the value to salt the hash. Cannot be {@code null}, but an empty ByteSource. * @param hashIterations the number of times to hash the credentials. At least one hash will always occur though, * even if this argument is 0 or negative. * @return the hashed value of the provided credentials, according to the specified salt and hash iterations. + * @throws NullPointerException if salt is {@code null}. */ protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) { String hashAlgorithmName = assertHashAlgorithmName(); - return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); + return new SimpleHash(hashAlgorithmName, credentials, requireNonNull(salt, "salt cannot be null."), hashIterations); } /** diff --git a/core/src/main/java/org/apache/shiro/authc/credential/Md2CredentialsMatcher.java b/core/src/main/java/org/apache/shiro/authc/credential/Md2CredentialsMatcher.java deleted file mode 100644 index c968df5afe..0000000000 --- a/core/src/main/java/org/apache/shiro/authc/credential/Md2CredentialsMatcher.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.shiro.authc.credential; - -import org.apache.shiro.crypto.hash.AbstractHash; -import org.apache.shiro.crypto.hash.Hash; -import org.apache.shiro.crypto.hash.Md2Hash; - - -/** - * {@code HashedCredentialsMatcher} implementation that expects the stored {@code AuthenticationInfo} credentials to be - * MD2 hashed. - *

- * Note: the MD2, MD5 and - * SHA-1 algorithms are now known to be vulnerable to - * compromise and/or collisions (read the linked pages for more). While most applications are ok with either of these - * two, if your application mandates high security, use the SHA-256 (or higher) hashing algorithms and their - * supporting CredentialsMatcher implementations.

- * - * @since 0.9 - * @deprecated since 1.1 - use the HashedCredentialsMatcher directly and set its - * {@link HashedCredentialsMatcher#setHashAlgorithmName(String) hashAlgorithmName} property. - */ -@Deprecated -public class Md2CredentialsMatcher extends HashedCredentialsMatcher { - - public Md2CredentialsMatcher() { - super(); - setHashAlgorithmName(Md2Hash.ALGORITHM_NAME); - } -} diff --git a/core/src/main/java/org/apache/shiro/authc/credential/Md5CredentialsMatcher.java b/core/src/main/java/org/apache/shiro/authc/credential/Md5CredentialsMatcher.java deleted file mode 100644 index 81b8f13c97..0000000000 --- a/core/src/main/java/org/apache/shiro/authc/credential/Md5CredentialsMatcher.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.shiro.authc.credential; - -import org.apache.shiro.crypto.hash.AbstractHash; -import org.apache.shiro.crypto.hash.Hash; -import org.apache.shiro.crypto.hash.Md5Hash; - - -/** - * {@code HashedCredentialsMatcher} implementation that expects the stored {@code AuthenticationInfo} credentials to be - * MD5 hashed. - *

- * Note: MD5 and - * SHA-1 algorithms are now known to be vulnerable to - * compromise and/or collisions (read the linked pages for more). While most applications are ok with either of these - * two, if your application mandates high security, use the SHA-256 (or higher) hashing algorithms and their - * supporting CredentialsMatcher implementations.

- * - * @since 0.9 - * @deprecated since 1.1 - use the HashedCredentialsMatcher directly and set its - * {@link HashedCredentialsMatcher#setHashAlgorithmName(String) hashAlgorithmName} property. - */ -public class Md5CredentialsMatcher extends HashedCredentialsMatcher { - - public Md5CredentialsMatcher() { - super(); - setHashAlgorithmName(Md5Hash.ALGORITHM_NAME); - } -} diff --git a/core/src/main/java/org/apache/shiro/authc/credential/PasswordMatcher.java b/core/src/main/java/org/apache/shiro/authc/credential/PasswordMatcher.java index e687dcc1a1..dd60a850b4 100644 --- a/core/src/main/java/org/apache/shiro/authc/credential/PasswordMatcher.java +++ b/core/src/main/java/org/apache/shiro/authc/credential/PasswordMatcher.java @@ -21,6 +21,7 @@ import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.crypto.hash.Hash; +import org.apache.shiro.lang.util.ByteSource; /** * A {@link CredentialsMatcher} that employs best-practices comparisons for hashed text passwords. @@ -39,6 +40,7 @@ public PasswordMatcher() { this.passwordService = new DefaultPasswordService(); } + @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { PasswordService service = ensurePasswordService(); @@ -49,23 +51,11 @@ public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo if (storedCredentials instanceof Hash) { Hash hashedPassword = (Hash)storedCredentials; - HashingPasswordService hashingService = assertHashingPasswordService(service); - return hashingService.passwordsMatch(submittedPassword, hashedPassword); + return hashedPassword.matchesPassword(ByteSource.Util.bytes(submittedPassword)); } //otherwise they are a String (asserted in the 'assertStoredCredentialsType' method call above): String formatted = (String)storedCredentials; - return passwordService.passwordsMatch(submittedPassword, formatted); - } - - private HashingPasswordService assertHashingPasswordService(PasswordService service) { - if (service instanceof HashingPasswordService) { - return (HashingPasswordService) service; - } - String msg = "AuthenticationInfo's stored credentials are a Hash instance, but the " + - "configured passwordService is not a " + - HashingPasswordService.class.getName() + " instance. This is required to perform Hash " + - "object password comparisons."; - throw new IllegalStateException(msg); + return service.passwordsMatch(submittedPassword, formatted); } private PasswordService ensurePasswordService() { diff --git a/core/src/main/java/org/apache/shiro/authc/credential/Sha1CredentialsMatcher.java b/core/src/main/java/org/apache/shiro/authc/credential/Sha1CredentialsMatcher.java deleted file mode 100644 index 6cdd328923..0000000000 --- a/core/src/main/java/org/apache/shiro/authc/credential/Sha1CredentialsMatcher.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.shiro.authc.credential; - -import org.apache.shiro.crypto.hash.AbstractHash; -import org.apache.shiro.crypto.hash.Hash; -import org.apache.shiro.crypto.hash.Sha1Hash; - - -/** - * {@code HashedCredentialsMatcher} implementation that expects the stored {@code AuthenticationInfo} credentials to be - * SHA hashed. - *

- * Note: MD5 and - * SHA-1 algorithms are now known to be vulnerable to - * compromise and/or collisions (read the linked pages for more). While most applications are ok with either of these - * two, if your application mandates high security, use the SHA-256 (or higher) hashing algorithms and their - * supporting CredentialsMatcher implementations.

- * - * @since 0.9 - * @deprecated since 1.1 - use the HashedCredentialsMatcher directly and set its - * {@link HashedCredentialsMatcher#setHashAlgorithmName(String) hashAlgorithmName} property. - */ -public class Sha1CredentialsMatcher extends HashedCredentialsMatcher { - - public Sha1CredentialsMatcher() { - super(); - setHashAlgorithmName(Sha1Hash.ALGORITHM_NAME); - } -} diff --git a/core/src/main/java/org/apache/shiro/realm/text/TextConfigurationRealm.java b/core/src/main/java/org/apache/shiro/realm/text/TextConfigurationRealm.java index 0439f93bb1..53d16ed756 100644 --- a/core/src/main/java/org/apache/shiro/realm/text/TextConfigurationRealm.java +++ b/core/src/main/java/org/apache/shiro/realm/text/TextConfigurationRealm.java @@ -184,6 +184,7 @@ protected void processUserDefinitions(Map userDefs) { String[] passwordAndRolesArray = StringUtils.split(value); + // the first token is expected to be the password. String password = passwordAndRolesArray[0]; SimpleAccount account = getUser(username); diff --git a/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy b/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy index 5365e75b7d..ffd3ad8ca2 100644 --- a/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy +++ b/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy @@ -23,10 +23,12 @@ import org.apache.shiro.crypto.hash.* import org.apache.shiro.crypto.hash.format.HashFormatFactory import org.apache.shiro.crypto.hash.format.HexFormat import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat -import org.junit.Test +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.function.Executable import static org.easymock.EasyMock.* -import static org.junit.Assert.* +import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the {@link DefaultPasswordService} implementation. @@ -36,41 +38,42 @@ import static org.junit.Assert.* class DefaultPasswordServiceTest { @Test + @DisplayName("throws NPE if plaintext is null") void testEncryptPasswordWithNullArgument() { - def service = new DefaultPasswordService() - assertNull service.encryptPassword(null) + def service = createSha256Service() + + assertThrows(NullPointerException, { service.encryptPassword(null) } as Executable) } @Test void testHashPasswordWithNullArgument() { - def service = new DefaultPasswordService() + def service = createSha256Service() assertNull service.hashPassword(null) } @Test void testEncryptPasswordDefault() { - def service = new DefaultPasswordService() + def service = createSha256Service() def encrypted = service.encryptPassword("12345") assertTrue service.passwordsMatch("12345", encrypted) } @Test void testEncryptPasswordWithInvalidMatch() { - def service = new DefaultPasswordService() + def service = createSha256Service() def encrypted = service.encryptPassword("ABCDEF") assertFalse service.passwordsMatch("ABC", encrypted) } @Test void testBackwardsCompatibility() { - def service = new DefaultPasswordService() + def service = createSha256Service() def encrypted = service.encryptPassword("12345") def submitted = "12345" assertTrue service.passwordsMatch(submitted, encrypted); //change some settings: - service.hashService.hashAlgorithmName = "MD5" - service.hashService.hashIterations = 250000 + service.hashService.defaultAlgorithmName = "SHA-512" def encrypted2 = service.encryptPassword(submitted) @@ -81,7 +84,7 @@ class DefaultPasswordServiceTest { @Test void testHashFormatWarned() { - def service = new DefaultPasswordService() + def service = createSha256Service() service.hashFormat = new HexFormat() assertTrue service.hashFormat instanceof HexFormat service.encryptPassword("test") @@ -90,13 +93,13 @@ class DefaultPasswordServiceTest { @Test void testPasswordsMatchWithNullOrEmpty() { - def service = new DefaultPasswordService() + def service = createSha256Service() assertTrue service.passwordsMatch(null, (String) null) assertTrue service.passwordsMatch(null, (Hash) null) assertTrue service.passwordsMatch("", (String) null) assertTrue service.passwordsMatch(null, "") assertFalse service.passwordsMatch(null, "12345") - assertFalse service.passwordsMatch(null, new Sha1Hash("test")) + assertFalse service.passwordsMatch(null, new Sha384Hash("test")) } @Test @@ -140,19 +143,6 @@ class DefaultPasswordServiceTest { verify factory } - @Test - void testStringComparisonWhenNotUsingAParsableHashFormat() { - - def service = new DefaultPasswordService() - service.hashFormat = new HexFormat() - //can't use random salts when using HexFormat: - service.hashService.generatePublicSalt = false - - def formatted = service.encryptPassword("12345") - - assertTrue service.passwordsMatch("12345", formatted) - } - @Test void testTurkishLocal() { @@ -162,7 +152,7 @@ class DefaultPasswordServiceTest { Locale.setDefault(new Locale("tr", "TR")); try { - PasswordService passwordService = new DefaultPasswordService(); + PasswordService passwordService = createSha256Service() String password = "333"; String enc = passwordService.encryptPassword(password); assertTrue(passwordService.passwordsMatch(password, enc)); @@ -171,4 +161,9 @@ class DefaultPasswordServiceTest { Locale.setDefault(locale); } } + + private static DefaultPasswordService createSha256Service() { + def hashService = new DefaultHashService(defaultAlgorithmName: 'SHA-256') + new DefaultPasswordService(hashService: hashService) + } } diff --git a/core/src/test/groovy/org/apache/shiro/authc/credential/PasswordMatcherTest.groovy b/core/src/test/groovy/org/apache/shiro/authc/credential/PasswordMatcherTest.groovy index 59d5530935..d900d6feca 100644 --- a/core/src/test/groovy/org/apache/shiro/authc/credential/PasswordMatcherTest.groovy +++ b/core/src/test/groovy/org/apache/shiro/authc/credential/PasswordMatcherTest.groovy @@ -20,8 +20,12 @@ package org.apache.shiro.authc.credential import org.apache.shiro.authc.AuthenticationInfo import org.apache.shiro.authc.AuthenticationToken +import org.apache.shiro.authc.SimpleAuthenticationInfo +import org.apache.shiro.authc.UsernamePasswordToken import org.apache.shiro.crypto.hash.Sha256Hash +import org.apache.shiro.crypto.hash.format.Shiro2CryptFormat import org.junit.Test +import org.junit.jupiter.api.DisplayName import static org.easymock.EasyMock.* import static org.junit.Assert.* @@ -87,11 +91,7 @@ class PasswordMatcherTest { matcher.passwordService = service assertSame service, matcher.passwordService - try { - assertTrue matcher.doCredentialsMatch(token, info) - fail "matcher should fail since PasswordService is not a HashingPasswordService" - } catch (IllegalStateException expected) { - } + assertTrue matcher.doCredentialsMatch(token, info) verify token, info, service } @@ -108,8 +108,6 @@ class PasswordMatcherTest { expect(token.credentials).andReturn submittedPassword expect(info.credentials).andReturn savedPassword - expect(service.passwordsMatch(submittedPassword, savedPassword)).andReturn true - replay token, info, service def matcher = new PasswordMatcher() @@ -175,7 +173,44 @@ class PasswordMatcherTest { } verify token, info, service + } + @Test + @DisplayName("test whether shiro2 bcrypt password can be parsed and matched.") + void testBCryptPassword() { + // given + def matcher = new PasswordMatcher(); + def bcryptPw = '$shiro2$2y$10$7rOjsAf2U/AKKqpMpCIn6etuOXyQ86tp2Tn9xv6FyXl2T0QYc3.G.' + def bcryptHash = new Shiro2CryptFormat().parse(bcryptPw); + def plaintext = 'secret#shiro,password;Jo8opech' + def principal = "user" + def usernamePasswordToken = new UsernamePasswordToken(principal, plaintext) + def authenticationInfo = new SimpleAuthenticationInfo(principal, bcryptHash, "inirealm") + + // when + def match = matcher.doCredentialsMatch(usernamePasswordToken, authenticationInfo) + + // then + assertTrue match + } + + @Test + @DisplayName("test whether shiro2 argon2 password can be parsed and matched.") + void testArgon2Password() { + // given + def matcher = new PasswordMatcher(); + def bcryptPw = '$shiro2$argon2id$v=19$m=4096,t=3,p=4$MTIzNDU2Nzg5MDEyMzQ1Ng$bjcHqfb0LPHyS13eVaNcBga9LF12I3k34H5ULt2gyoI' + def bcryptHash = new Shiro2CryptFormat().parse(bcryptPw); + def plaintext = 'secret#shiro,password;Jo8opech' + def principal = "user" + def usernamePasswordToken = new UsernamePasswordToken(principal, plaintext) + def authenticationInfo = new SimpleAuthenticationInfo(principal, bcryptHash, "inirealm") + + // when + def match = matcher.doCredentialsMatch(usernamePasswordToken, authenticationInfo) + + // then + assertTrue match } } diff --git a/core/src/test/java/org/apache/shiro/authc/credential/HashedCredentialsMatcherTest.java b/core/src/test/java/org/apache/shiro/authc/credential/HashedCredentialsMatcherTest.java index 6c9891f235..100a9c8895 100644 --- a/core/src/test/java/org/apache/shiro/authc/credential/HashedCredentialsMatcherTest.java +++ b/core/src/test/java/org/apache/shiro/authc/credential/HashedCredentialsMatcherTest.java @@ -23,10 +23,10 @@ import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.crypto.SecureRandomNumberGenerator; -import org.apache.shiro.crypto.hash.Sha1Hash; +import org.apache.shiro.crypto.hash.Sha512Hash; +import org.apache.shiro.lang.util.ByteSource; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.SimplePrincipalCollection; -import org.apache.shiro.lang.util.ByteSource; import org.junit.Test; import static org.junit.Assert.assertTrue; @@ -43,11 +43,11 @@ public class HashedCredentialsMatcherTest { @Test public void testSaltedAuthenticationInfo() { //use SHA-1 hashing in this test: - HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(Sha1Hash.ALGORITHM_NAME); + HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(Sha512Hash.ALGORITHM_NAME); //simulate a user account with a SHA-1 hashed and salted password: ByteSource salt = new SecureRandomNumberGenerator().nextBytes(); - Object hashedPassword = new Sha1Hash("password", salt); + Object hashedPassword = new Sha512Hash("password", salt); SimpleAuthenticationInfo account = new SimpleAuthenticationInfo("username", hashedPassword, salt, "realmName"); //simulate a username/password (plaintext) token created in response to a login attempt: @@ -63,17 +63,21 @@ public void testSaltedAuthenticationInfo() { */ @Test public void testBackwardsCompatibleUnsaltedAuthenticationInfo() { - HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(Sha1Hash.ALGORITHM_NAME); + HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(Sha512Hash.ALGORITHM_NAME); //simulate an account with SHA-1 hashed password (no salt) final String username = "username"; final String password = "password"; - final Object hashedPassword = new Sha1Hash(password).getBytes(); + final Object hashedPassword = new Sha512Hash(password).getBytes(); AuthenticationInfo account = new AuthenticationInfo() { + private static final long serialVersionUID = -3613684957517438801L; + + @Override public PrincipalCollection getPrincipals() { return new SimplePrincipalCollection(username, "realmName"); } + @Override public Object getCredentials() { return hashedPassword; } @@ -92,7 +96,7 @@ public Object getCredentials() { */ @Test public void testBackwardsCompatibleSaltedAuthenticationInfo() { - HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(Sha1Hash.ALGORITHM_NAME); + HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(Sha512Hash.ALGORITHM_NAME); //enable this for Shiro 1.0 backwards compatibility: matcher.setHashSalted(true); @@ -100,12 +104,16 @@ public void testBackwardsCompatibleSaltedAuthenticationInfo() { //(BAD IDEA, but backwards-compatible): final String username = "username"; final String password = "password"; - final Object hashedPassword = new Sha1Hash(password, username).getBytes(); + final Object hashedPassword = new Sha512Hash(password, username).getBytes(); AuthenticationInfo account = new AuthenticationInfo() { + private static final long serialVersionUID = -6942549615727484358L; + + @Override public PrincipalCollection getPrincipals() { return new SimplePrincipalCollection(username, "realmName"); } + @Override public Object getCredentials() { return hashedPassword; } diff --git a/core/src/test/java/org/apache/shiro/authc/credential/Md2CredentialsMatcherTest.java b/core/src/test/java/org/apache/shiro/authc/credential/Md2CredentialsMatcherTest.java deleted file mode 100644 index 5286a58a2e..0000000000 --- a/core/src/test/java/org/apache/shiro/authc/credential/Md2CredentialsMatcherTest.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.shiro.authc.credential; - -import org.apache.shiro.crypto.hash.AbstractHash; -import org.apache.shiro.crypto.hash.Md2Hash; - - -/** - * @since Jun 10, 2008 4:38:16 PM - */ -public class Md2CredentialsMatcherTest extends AbstractHashedCredentialsMatcherTest { - - public Class getMatcherClass() { - return Md2CredentialsMatcher.class; - } - - public AbstractHash hash(Object credentials) { - return new Md2Hash(credentials); - } -} - - diff --git a/core/src/test/java/org/apache/shiro/authc/credential/Md5CredentialsMatcherTest.java b/core/src/test/java/org/apache/shiro/authc/credential/Md5CredentialsMatcherTest.java deleted file mode 100644 index 4c9d71dc32..0000000000 --- a/core/src/test/java/org/apache/shiro/authc/credential/Md5CredentialsMatcherTest.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.shiro.authc.credential; - -import org.apache.shiro.crypto.hash.AbstractHash; -import org.apache.shiro.crypto.hash.Md5Hash; - - -/** - * @since Jun 10, 2008 4:59:36 PM - */ -public class Md5CredentialsMatcherTest extends AbstractHashedCredentialsMatcherTest { - - public Class getMatcherClass() { - return Md5CredentialsMatcher.class; - } - - public AbstractHash hash(Object credentials) { - return new Md5Hash(credentials); - } -} \ No newline at end of file diff --git a/core/src/test/java/org/apache/shiro/authc/credential/Sha1CredentialsMatcherTest.java b/core/src/test/java/org/apache/shiro/authc/credential/Sha1CredentialsMatcherTest.java deleted file mode 100644 index 29d6283658..0000000000 --- a/core/src/test/java/org/apache/shiro/authc/credential/Sha1CredentialsMatcherTest.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.shiro.authc.credential; - -import org.apache.shiro.crypto.hash.AbstractHash; -import org.apache.shiro.crypto.hash.Sha1Hash; - - -/** - * @since Jun 10, 2008 5:00:30 PM - */ -public class Sha1CredentialsMatcherTest extends AbstractHashedCredentialsMatcherTest { - - public Class getMatcherClass() { - return Sha1CredentialsMatcher.class; - } - - public AbstractHash hash(Object credentials) { - return new Sha1Hash(credentials); - } -} diff --git a/crypto/cipher/pom.xml b/crypto/cipher/pom.xml index 72974d4614..2b03bfd2b8 100644 --- a/crypto/cipher/pom.xml +++ b/crypto/cipher/pom.xml @@ -65,7 +65,6 @@ org.bouncycastle bcprov-jdk15on - 1.64 test diff --git a/crypto/hash/pom.xml b/crypto/hash/pom.xml index 6526345c27..e5503afab8 100644 --- a/crypto/hash/pom.xml +++ b/crypto/hash/pom.xml @@ -61,6 +61,11 @@ org.apache.shiro shiro-crypto-core
+ + + org.bouncycastle + bcprov-jdk15on + diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/AbstractCryptHash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/AbstractCryptHash.java new file mode 100644 index 0000000000..f056c617ff --- /dev/null +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/AbstractCryptHash.java @@ -0,0 +1,253 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.shiro.crypto.hash; + +import org.apache.shiro.lang.codec.Base64; +import org.apache.shiro.lang.codec.Hex; +import org.apache.shiro.lang.util.ByteSource; + +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Locale; +import java.util.Objects; +import java.util.StringJoiner; +import java.util.regex.Pattern; + +import static java.util.Objects.requireNonNull; + +/** + * Abstract class for hashes following the posix crypt(3) format. + * + *

These implementations must contain a salt, a salt length, can format themselves to a valid String + * suitable for the {@code /etc/shadow} file.

+ * + *

It also defines the hex and base64 output by wrapping the output of {@link #formatToCryptString()}.

+ * + *

Implementation notice: Implementations should provide a static {@code fromString()} method.

+ * + * @since 2.0 + */ +public abstract class AbstractCryptHash implements Hash, Serializable { + + private static final long serialVersionUID = 2483214646921027859L; + + protected static final Pattern DELIMITER = Pattern.compile("\\$"); + + private final String algorithmName; + private final byte[] hashedData; + private final ByteSource salt; + + /** + * Cached value of the {@link #toHex() toHex()} call so multiple calls won't incur repeated overhead. + */ + private String hexEncoded; + /** + * Cached value of the {@link #toBase64() toBase64()} call so multiple calls won't incur repeated overhead. + */ + private String base64Encoded; + + /** + * Constructs an {@link AbstractCryptHash} using the algorithm name, hashed data and salt parameters. + * + *

Other required parameters must be stored by the implementation.

+ * + * @param algorithmName internal algorithm name, e.g. {@code 2y} for bcrypt and {@code argon2id} for argon2. + * @param hashedData the hashed data as a byte array. Does not include the salt or other parameters. + * @param salt the salt which was used when generating the hash. + * @throws IllegalArgumentException if the salt is not the same size as {@link #getSaltLength()}. + */ + public AbstractCryptHash(final String algorithmName, final byte[] hashedData, final ByteSource salt) { + this.algorithmName = algorithmName; + this.hashedData = Arrays.copyOf(hashedData, hashedData.length); + this.salt = requireNonNull(salt); + checkValid(); + } + + protected final void checkValid() { + checkValidAlgorithm(); + + checkValidSalt(); + } + + /** + * Algorithm-specific checks of the algorithm’s parameters. + * + *

While the salt length will be checked by default, other checks will be useful. + * Examples are: Argon2 checking for the memory and parallelism parameters, bcrypt checking + * for the cost parameters being in a valid range.

+ * + * @throws IllegalArgumentException if any of the parameters are invalid. + */ + protected abstract void checkValidAlgorithm(); + + /** + * Default check method for a valid salt. Can be overridden, because multiple salt lengths could be valid. + * + * By default, this method checks if the number of bytes in the salt + * are equal to the int returned by {@link #getSaltLength()}. + * + * @throws IllegalArgumentException if the salt length does not match the returned value of {@link #getSaltLength()}. + */ + protected void checkValidSalt() { + int length = salt.getBytes().length; + if (length != getSaltLength()) { + String message = String.format( + Locale.ENGLISH, + "Salt length is expected to be [%d] bytes, but was [%d] bytes.", + getSaltLength(), + length + ); + throw new IllegalArgumentException(message); + } + } + + /** + * Implemented by subclasses, this specifies the KDF algorithm name + * to use when performing the hash. + * + *

When multiple algorithm names are acceptable, then this method should return the primary algorithm name.

+ * + *

Example: Bcrypt hashed can be identified by {@code 2y} and {@code 2a}. The method will return {@code 2y} + * for newly generated hashes by default, unless otherwise overridden.

+ * + * @return the KDF algorithm name to use when performing the hash. + */ + @Override + public String getAlgorithmName() { + return this.algorithmName; + } + + /** + * The length in number of bytes of the salt which is needed for this algorithm. + * + * @return the expected length of the salt (in bytes). + */ + public abstract int getSaltLength(); + + @Override + public ByteSource getSalt() { + return this.salt; + } + + /** + * Returns only the hashed data. Those are of no value on their own. If you need to serialize + * the hash, please refer to {@link #formatToCryptString()}. + * + * @return A copy of the hashed data as bytes. + * @see #formatToCryptString() + */ + @Override + public byte[] getBytes() { + return Arrays.copyOf(this.hashedData, this.hashedData.length); + } + + @Override + public boolean isEmpty() { + return false; + } + + /** + * Returns a hex-encoded string of the underlying {@link #formatToCryptString()} formatted output}. + *

+ * This implementation caches the resulting hex string so multiple calls to this method remain efficient. + * + * @return a hex-encoded string of the underlying {@link #formatToCryptString()} formatted output}. + */ + @Override + public String toHex() { + if (this.hexEncoded == null) { + this.hexEncoded = Hex.encodeToString(this.formatToCryptString().getBytes(StandardCharsets.UTF_8)); + } + return this.hexEncoded; + } + + /** + * Returns a Base64-encoded string of the underlying {@link #formatToCryptString()} formatted output}. + *

+ * This implementation caches the resulting Base64 string so multiple calls to this method remain efficient. + * + * @return a Base64-encoded string of the underlying {@link #formatToCryptString()} formatted output}. + */ + @Override + public String toBase64() { + if (this.base64Encoded == null) { + //cache result in case this method is called multiple times. + this.base64Encoded = Base64.encodeToString(this.formatToCryptString().getBytes(StandardCharsets.UTF_8)); + } + return this.base64Encoded; + } + + /** + * This method MUST return a single-lined string which would also be recognizable by + * a posix {@code /etc/passwd} file. + * + * @return a formatted string, e.g. {@code $2y$10$7rOjsAf2U/AKKqpMpCIn6e$tuOXyQ86tp2Tn9xv6FyXl2T0QYc3.G.} for bcrypt. + */ + public abstract String formatToCryptString(); + + /** + * Returns {@code true} if the specified object is an AbstractCryptHash and its + * {@link #formatToCryptString()} formatted output} is identical to + * this AbstractCryptHash's formatted output, {@code false} otherwise. + * + * @param other the object (AbstractCryptHash) to check for equality. + * @return {@code true} if the specified object is a AbstractCryptHash + * and its {@link #formatToCryptString()} formatted output} is identical to + * this AbstractCryptHash's formatted output, {@code false} otherwise. + */ + @Override + public boolean equals(final Object other) { + if (other instanceof AbstractCryptHash) { + final AbstractCryptHash that = (AbstractCryptHash) other; + return this.formatToCryptString().equals(that.formatToCryptString()); + } + return false; + } + + /** + * Hashes the formatted crypt string. + * + *

Implementations should not override this method, as different algorithms produce different output formats + * and require different parameters.

+ * @return a hashcode from the {@link #formatToCryptString() formatted output}. + */ + @Override + public int hashCode() { + return Objects.hash(this.formatToCryptString()); + } + + /** + * Simple implementation that merely returns {@link #toHex() toHex()}. + * + * @return the {@link #toHex() toHex()} value. + */ + @Override + public String toString() { + return new StringJoiner(", ", AbstractCryptHash.class.getSimpleName() + "[", "]") + .add("super=" + super.toString()) + .add("algorithmName='" + algorithmName + "'") + .add("hashedData=" + Arrays.toString(hashedData)) + .add("salt=" + salt) + .add("hexEncoded='" + hexEncoded + "'") + .add("base64Encoded='" + base64Encoded + "'") + .toString(); + } +} diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/AbstractHash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/AbstractHash.java index 4bf8373ae0..684647c7dc 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/AbstractHash.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/AbstractHash.java @@ -18,11 +18,11 @@ */ package org.apache.shiro.crypto.hash; +import org.apache.shiro.crypto.UnknownAlgorithmException; import org.apache.shiro.lang.codec.Base64; import org.apache.shiro.lang.codec.CodecException; import org.apache.shiro.lang.codec.CodecSupport; import org.apache.shiro.lang.codec.Hex; -import org.apache.shiro.crypto.UnknownAlgorithmException; import java.io.Serializable; import java.security.MessageDigest; @@ -46,6 +46,7 @@ @Deprecated public abstract class AbstractHash extends CodecSupport implements Hash, Serializable { + private static final long serialVersionUID = -4723044219611288405L; /** * The hashed data */ @@ -142,8 +143,10 @@ public AbstractHash(Object source, Object salt, int hashIterations) throws Codec * * @return the {@link MessageDigest MessageDigest} algorithm name to use when performing the hash. */ + @Override public abstract String getAlgorithmName(); + @Override public byte[] getBytes() { return this.bytes; } @@ -233,6 +236,7 @@ protected byte[] hash(byte[] bytes, byte[] salt, int hashIterations) throws Unkn * * @return a hex-encoded string of the underlying {@link #getBytes byte array}. */ + @Override public String toHex() { if (this.hexEncoded == null) { this.hexEncoded = Hex.encodeToString(getBytes()); @@ -249,6 +253,7 @@ public String toHex() { * * @return a Base64-encoded string of the underlying {@link #getBytes byte array}. */ + @Override public String toBase64() { if (this.base64Encoded == null) { //cache result in case this method is called multiple times. @@ -262,6 +267,7 @@ public String toBase64() { * * @return the {@link #toHex() toHex()} value. */ + @Override public String toString() { return toHex(); } @@ -274,6 +280,7 @@ public String toString() { * @return {@code true} if the specified object is a Hash and its {@link #getBytes byte array} is identical to * this Hash's byte array, {@code false} otherwise. */ + @Override public boolean equals(Object o) { if (o instanceof Hash) { Hash other = (Hash) o; @@ -287,6 +294,7 @@ public boolean equals(Object o) { * * @return toHex().hashCode() */ + @Override public int hashCode() { if (this.bytes == null || this.bytes.length == 0) { return 0; @@ -294,68 +302,4 @@ public int hashCode() { return Arrays.hashCode(this.bytes); } - private static void printMainUsage(Class clazz, String type) { - System.out.println("Prints an " + type + " hash value."); - System.out.println("Usage: java " + clazz.getName() + " [-base64] [-salt ] [-times ] "); - System.out.println("Options:"); - System.out.println("\t-base64\t\tPrints the hash value as a base64 String instead of the default hex."); - System.out.println("\t-salt\t\tSalts the hash with the specified "); - System.out.println("\t-times\t\tHashes the input number of times"); - } - - private static boolean isReserved(String arg) { - return "-base64".equals(arg) || "-times".equals(arg) || "-salt".equals(arg); - } - - static int doMain(Class clazz, String[] args) { - String simple = clazz.getSimpleName(); - int index = simple.indexOf("Hash"); - String type = simple.substring(0, index).toUpperCase(); - - if (args == null || args.length < 1 || args.length > 7) { - printMainUsage(clazz, type); - return -1; - } - boolean hex = true; - String salt = null; - int times = 1; - String text = args[args.length - 1]; - for (int i = 0; i < args.length; i++) { - String arg = args[i]; - if (arg.equals("-base64")) { - hex = false; - } else if (arg.equals("-salt")) { - if ((i + 1) >= (args.length - 1)) { - String msg = "Salt argument must be followed by a salt value. The final argument is " + - "reserved for the value to hash."; - System.out.println(msg); - printMainUsage(clazz, type); - return -1; - } - salt = args[i + 1]; - } else if (arg.equals("-times")) { - if ((i + 1) >= (args.length - 1)) { - String msg = "Times argument must be followed by an integer value. The final argument is " + - "reserved for the value to hash"; - System.out.println(msg); - printMainUsage(clazz, type); - return -1; - } - try { - times = Integer.valueOf(args[i + 1]); - } catch (NumberFormatException e) { - String msg = "Times argument must be followed by an integer value."; - System.out.println(msg); - printMainUsage(clazz, type); - return -1; - } - } - } - - Hash hash = new Md2Hash(text, salt, times); - String hashed = hex ? hash.toHex() : hash.toBase64(); - System.out.print(hex ? "Hex: " : "Base64: "); - System.out.println(hashed); - return 0; - } } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/ConfigurableHashService.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/ConfigurableHashService.java index fd7883f65f..6e4dca5c4c 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/ConfigurableHashService.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/ConfigurableHashService.java @@ -18,9 +18,6 @@ */ package org.apache.shiro.crypto.hash; -import org.apache.shiro.crypto.RandomNumberGenerator; -import org.apache.shiro.lang.util.ByteSource; - /** * A {@code HashService} that allows configuration of its strategy via JavaBeans-compatible setter methods. * @@ -29,33 +26,12 @@ public interface ConfigurableHashService extends HashService { /** - * Sets the 'private' (internal) salt to be paired with a 'public' (random or supplied) salt during hash computation. - * - * @param privateSalt the 'private' internal salt to be paired with a 'public' (random or supplied) salt during - * hash computation. - */ - void setPrivateSalt(ByteSource privateSalt); - - /** - * Sets the number of hash iterations that will be performed during hash computation. - * - * @param iterations the number of hash iterations that will be performed during hash computation. - */ - void setHashIterations(int iterations); - - /** - * Sets the name of the {@link java.security.MessageDigest MessageDigest} algorithm that will be used to compute - * hashes. + * Sets the name of the key derivation function algorithm that will be used to compute + * secure hashes for passwords. * - * @param name the name of the {@link java.security.MessageDigest MessageDigest} algorithm that will be used to - * compute hashes. + * @param name the name of the key derivation function algorithm that will be used to + * compute secure hashes for passwords. */ - void setHashAlgorithmName(String name); + void setDefaultAlgorithmName(String name); - /** - * Sets a source of randomness used to generate public salts that will in turn be used during hash computation. - * - * @param rng a source of randomness used to generate public salts that will in turn be used during hash computation. - */ - void setRandomNumberGenerator(RandomNumberGenerator rng); } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/DefaultHashService.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/DefaultHashService.java index 486e19df79..ed2653f1c6 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/DefaultHashService.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/DefaultHashService.java @@ -18,39 +18,19 @@ */ package org.apache.shiro.crypto.hash; -import org.apache.shiro.crypto.RandomNumberGenerator; -import org.apache.shiro.crypto.SecureRandomNumberGenerator; -import org.apache.shiro.lang.util.ByteSource; +import java.security.SecureRandom; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Random; /** - * Default implementation of the {@link HashService} interface, supporting a customizable hash algorithm name, - * secure-random salt generation, multiple hash iterations and an optional internal - * {@link #setPrivateSalt(ByteSource) privateSalt}. + * Default implementation of the {@link HashService} interface, supporting a customizable hash algorithm name. *

Hash Algorithm

- * You may specify a hash algorithm via the {@link #setHashAlgorithmName(String)} property. Any algorithm name + * You may specify a hash algorithm via the {@link #setDefaultAlgorithmName(String)} property. Any algorithm name * understood by the JDK * {@link java.security.MessageDigest#getInstance(String) MessageDigest.getInstance(String algorithmName)} method - * will work. The default is {@code SHA-512}. - *

Random Salts

- * When a salt is not specified in a request, this implementation generates secure random salts via its - * {@link #setRandomNumberGenerator(org.apache.shiro.crypto.RandomNumberGenerator) randomNumberGenerator} property. - * Random salts (and potentially combined with the internal {@link #getPrivateSalt() privateSalt}) is a very strong - * salting strategy, as salts should ideally never be based on known/guessable data. The default instance is a - * {@link SecureRandomNumberGenerator}. - *

Hash Iterations

- * Secure hashing strategies often employ multiple hash iterations to slow down the hashing process. This technique - * is usually used for password hashing, since the longer it takes to compute a password hash, the longer it would - * take for an attacker to compromise a password. This - * blog article - * explains in greater detail why this is useful, as well as information on how many iterations is 'enough'. - *

- * You may set the number of hash iterations via the {@link #setHashIterations(int)} property. The default is - * {@code 1}, but should be increased significantly if the {@code HashService} is intended to be used for password - * hashing. See the linked blog article for more info. - *

Private Salt

- * If using this implementation as part of a password hashing strategy, it might be desirable to configure a - * {@link #setPrivateSalt(ByteSource) private salt}: - *

+ * will work, or any Hash algorithm implemented by any loadable {@link HashSpi}. The default is {@code argon2}. + *

* A hash and the salt used to compute it are often stored together. If an attacker is ever able to access * the hash (e.g. during password cracking) and it has the full salt value, the attacker has all of the input necessary * to try to brute-force crack the hash (source + complete salt). @@ -58,60 +38,28 @@ * However, if part of the salt is not available to the attacker (because it is not stored with the hash), it is * much harder to crack the hash value since the attacker does not have the complete inputs necessary. *

- * The {@link #getPrivateSalt() privateSalt} property exists to satisfy this private-and-not-shared part of the salt. - * If you configure this attribute, you can obtain this additional very important safety feature. - *

- * *By default, the {@link #getPrivateSalt() privateSalt} is null, since a sensible default cannot be used that - * isn't easily compromised (because Shiro is an open-source project and any default could be easily seen and used). * * @since 1.2 */ public class DefaultHashService implements ConfigurableHashService { - /** - * The RandomNumberGenerator to use to randomly generate the public part of the hash salt. - */ - private RandomNumberGenerator rng; + private final Random random; /** * The MessageDigest name of the hash algorithm to use for computing hashes. */ - private String algorithmName; - - /** - * The 'private' part of the hash salt. - */ - private ByteSource privateSalt; - - /** - * The number of hash iterations to perform when computing hashes. - */ - private int iterations; + private String defaultAlgorithmName; - /** - * Whether or not to generate public salts if a request does not provide one. - */ - private boolean generatePublicSalt; /** * Constructs a new {@code DefaultHashService} instance with the following defaults: *

    - *
  • {@link #setHashAlgorithmName(String) hashAlgorithmName} = {@code SHA-512}
  • - *
  • {@link #setHashIterations(int) hashIterations} = {@code 1}
  • - *
  • {@link #setRandomNumberGenerator(org.apache.shiro.crypto.RandomNumberGenerator) randomNumberGenerator} = - * new {@link SecureRandomNumberGenerator}()
  • - *
  • {@link #setGeneratePublicSalt(boolean) generatePublicSalt} = {@code false}
  • + *
  • {@link #setDefaultAlgorithmName(String) hashAlgorithmName} = {@code SHA-512}
  • *
- *

- * If this hashService will be used for password hashing it is recommended to set the - * {@link #setPrivateSalt(ByteSource) privateSalt} and significantly increase the number of - * {@link #setHashIterations(int) hashIterations}. See the class-level JavaDoc for more information. */ public DefaultHashService() { - this.algorithmName = "SHA-512"; - this.iterations = 1; - this.generatePublicSalt = false; - this.rng = new SecureRandomNumberGenerator(); + this.random = new SecureRandom(); + this.defaultAlgorithmName = "argon2"; } /** @@ -123,222 +71,45 @@ public DefaultHashService() { *

* A salt will be generated and used to compute the hash. The salt is generated as follows: *

    - *
  1. Use the {@link #getRandomNumberGenerator() randomNumberGenerator} to generate a new random number.
  2. - *
  3. {@link #combine(ByteSource, ByteSource) combine} this random salt with any configured - * {@link #getPrivateSalt() privateSalt} - *
  4. *
  5. Use the combined value as the salt used during hash computation
  6. *
* *
  • - * If the request salt is not null: - *

    - * This indicates that the hash computation is for comparison purposes (of a - * previously computed hash). The request salt will be {@link #combine(ByteSource, ByteSource) combined} with any - * configured {@link #getPrivateSalt() privateSalt} and used as the complete salt during hash computation. - *

  • - * - *

    - * The returned {@code Hash}'s {@link Hash#getSalt() salt} property - * will contain only the 'public' part of the salt and NOT the privateSalt. See the class-level - * JavaDoc explanation for more info. * * @param request the request to process * @return the response containing the result of the hash computation, as well as any hash salt used that should be * exposed to the caller. */ + @Override public Hash computeHash(HashRequest request) { if (request == null || request.getSource() == null || request.getSource().isEmpty()) { return null; } String algorithmName = getAlgorithmName(request); - ByteSource source = request.getSource(); - int iterations = getIterations(request); - - ByteSource publicSalt = getPublicSalt(request); - ByteSource privateSalt = getPrivateSalt(); - ByteSource salt = combine(privateSalt, publicSalt); - Hash computed = new SimpleHash(algorithmName, source, salt, iterations); + Optional kdfHash = HashProvider.getByAlgorithmName(algorithmName); + if (kdfHash.isPresent()) { + HashSpi hashSpi = kdfHash.orElseThrow(NoSuchElementException::new); - SimpleHash result = new SimpleHash(algorithmName); - result.setBytes(computed.getBytes()); - result.setIterations(iterations); - //Only expose the public salt - not the real/combined salt that might have been used: - result.setSalt(publicSalt); - - return result; - } - - protected String getAlgorithmName(HashRequest request) { - String name = request.getAlgorithmName(); - if (name == null) { - name = getHashAlgorithmName(); + return hashSpi.newHashFactory(random).generate(request); } - return name; - } - protected int getIterations(HashRequest request) { - int iterations = Math.max(0, request.getIterations()); - if (iterations < 1) { - iterations = Math.max(1, getHashIterations()); - } - return iterations; + throw new UnsupportedOperationException("Cannot create a hash with the given algorithm: " + algorithmName); } - /** - * Returns the public salt that should be used to compute a hash based on the specified request or - * {@code null} if no public salt should be used. - *

    - * This implementation functions as follows: - *

      - *
    1. If the request salt is not null and non-empty, this will be used, return it.
    2. - *
    3. If the request salt is null or empty: - *
        - *
      1. If a private salt has been set OR {@link #isGeneratePublicSalt()} is {@code true}, - * auto generate a random public salt via the configured - * {@link #getRandomNumberGenerator() randomNumberGenerator}.
      2. - *
      3. If a private salt has not been configured and {@link #isGeneratePublicSalt()} is {@code false}, - * do nothing - return {@code null} to indicate a salt should not be used during hash computation.
      4. - *
      - *
    4. - *
    - * - * @param request request the request to process - * @return the public salt that should be used to compute a hash based on the specified request or - * {@code null} if no public salt should be used. - */ - protected ByteSource getPublicSalt(HashRequest request) { - - ByteSource publicSalt = request.getSalt(); - - if (publicSalt != null && !publicSalt.isEmpty()) { - //a public salt was explicitly requested to be used - go ahead and use it: - return publicSalt; - } - - publicSalt = null; - - //check to see if we need to generate one: - ByteSource privateSalt = getPrivateSalt(); - boolean privateSaltExists = privateSalt != null && !privateSalt.isEmpty(); - - //If a private salt exists, we must generate a public salt to protect the integrity of the private salt. - //Or generate it if the instance is explicitly configured to do so: - if (privateSaltExists || isGeneratePublicSalt()) { - publicSalt = getRandomNumberGenerator().nextBytes(); - } - return publicSalt; - } - - /** - * Combines the specified 'private' salt bytes with the specified additional extra bytes to use as the - * total salt during hash computation. {@code privateSaltBytes} will be {@code null} }if no private salt has been - * configured. - * - * @param privateSalt the (possibly {@code null}) 'private' salt to combine with the specified extra bytes - * @param publicSalt the extra bytes to use in addition to the given private salt. - * @return a combination of the specified private salt bytes and extra bytes that will be used as the total - * salt during hash computation. - */ - protected ByteSource combine(ByteSource privateSalt, ByteSource publicSalt) { - - byte[] privateSaltBytes = privateSalt != null ? privateSalt.getBytes() : null; - int privateSaltLength = privateSaltBytes != null ? privateSaltBytes.length : 0; - - byte[] publicSaltBytes = publicSalt != null ? publicSalt.getBytes() : null; - int extraBytesLength = publicSaltBytes != null ? publicSaltBytes.length : 0; - - int length = privateSaltLength + extraBytesLength; - - if (length <= 0) { - return null; - } - - byte[] combined = new byte[length]; - - int i = 0; - for (int j = 0; j < privateSaltLength; j++) { - assert privateSaltBytes != null; - combined[i++] = privateSaltBytes[j]; - } - for (int j = 0; j < extraBytesLength; j++) { - assert publicSaltBytes != null; - combined[i++] = publicSaltBytes[j]; - } - - return ByteSource.Util.bytes(combined); - } - - public void setHashAlgorithmName(String name) { - this.algorithmName = name; - } - - public String getHashAlgorithmName() { - return this.algorithmName; - } - - public void setPrivateSalt(ByteSource privateSalt) { - this.privateSalt = privateSalt; - } - - public ByteSource getPrivateSalt() { - return this.privateSalt; - } - - public void setHashIterations(int count) { - this.iterations = count; - } - - public int getHashIterations() { - return this.iterations; + protected String getAlgorithmName(HashRequest request) { + return request.getAlgorithmName().orElseGet(this::getDefaultAlgorithmName); } - public void setRandomNumberGenerator(RandomNumberGenerator rng) { - this.rng = rng; + @Override + public void setDefaultAlgorithmName(String name) { + this.defaultAlgorithmName = name; } - public RandomNumberGenerator getRandomNumberGenerator() { - return this.rng; + public String getDefaultAlgorithmName() { + return this.defaultAlgorithmName; } - /** - * Returns {@code true} if a public salt should be randomly generated and used to compute a hash if a - * {@link HashRequest} does not specify a salt, {@code false} otherwise. - *

    - * The default value is {@code false} but should definitely be set to {@code true} if the - * {@code HashService} instance is being used for password hashing. - *

    - * NOTE: this property only has an effect if a {@link #getPrivateSalt() privateSalt} is NOT configured. If a - * private salt has been configured and a request does not provide a salt, a random salt will always be generated - * to protect the integrity of the private salt (without a public salt, the private salt would be exposed as-is, - * which is undesirable). - * - * @return {@code true} if a public salt should be randomly generated and used to compute a hash if a - * {@link HashRequest} does not specify a salt, {@code false} otherwise. - */ - public boolean isGeneratePublicSalt() { - return generatePublicSalt; - } - - /** - * Sets whether or not a public salt should be randomly generated and used to compute a hash if a - * {@link HashRequest} does not specify a salt. - *

    - * The default value is {@code false} but should definitely be set to {@code true} if the - * {@code HashService} instance is being used for password hashing. - *

    - * NOTE: this property only has an effect if a {@link #getPrivateSalt() privateSalt} is NOT configured. If a - * private salt has been configured and a request does not provide a salt, a random salt will always be generated - * to protect the integrity of the private salt (without a public salt, the private salt would be exposed as-is, - * which is undesirable). - * - * @param generatePublicSalt whether or not a public salt should be randomly generated and used to compute a hash - * if a {@link HashRequest} does not specify a salt. - */ - public void setGeneratePublicSalt(boolean generatePublicSalt) { - this.generatePublicSalt = generatePublicSalt; - } } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Hash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Hash.java index 3e26928601..ce52ce8fd3 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Hash.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Hash.java @@ -28,9 +28,6 @@ * The bytes returned by the parent interface's {@link #getBytes() getBytes()} are the hashed value of the * original input source, also known as the 'checksum' or 'digest'. * - * @see Md2Hash - * @see Md5Hash - * @see Sha1Hash * @see Sha256Hash * @see Sha384Hash * @see Sha512Hash @@ -64,4 +61,13 @@ public interface Hash extends ByteSource { */ int getIterations(); + /** + * Tests if a given passwords matches with this instance. + * + *

    Usually implementations will re-create {@code this} but with the given plaintext bytes as secret.

    + * + * @param plaintextBytes the plaintext bytes from a user. + * @return {@code true} if the given plaintext generates an equal hash with the same parameters as from this hash. + */ + boolean matchesPassword(ByteSource plaintextBytes); } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashProvider.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashProvider.java new file mode 100644 index 0000000000..64de6f935b --- /dev/null +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashProvider.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.shiro.crypto.hash; + +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.stream.StreamSupport; + +import static java.util.Objects.requireNonNull; + +/** + * Hashes used by the Shiro2CryptFormat class. + * + *

    Instead of maintaining them as an {@code Enum}, ServiceLoaders would provide a pluggable alternative.

    + * + * @since 2.0 + */ +public final class HashProvider { + + private HashProvider() { + // utility class + } + + /** + * Find a KDF implementation by searching the algorithms. + * + * @param algorithmName the algorithmName to match. This is case-sensitive. + * @return an instance of {@link HashProvider} if found, otherwise {@link Optional#empty()}. + * @throws NullPointerException if the given parameter algorithmName is {@code null}. + */ + public static Optional getByAlgorithmName(String algorithmName) { + requireNonNull(algorithmName, "algorithmName in HashProvider.getByAlgorithmName"); + ServiceLoader hashSpis = load(); + + return StreamSupport.stream(hashSpis.spliterator(), false) + .filter(hashSpi -> hashSpi.getImplementedAlgorithms().contains(algorithmName)) + .findAny(); + } + + @SuppressWarnings("unchecked") + private static ServiceLoader load() { + return ServiceLoader.load(HashSpi.class); + } + +} diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashRequest.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashRequest.java index 79d32514f3..2f0232c1c1 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashRequest.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashRequest.java @@ -19,6 +19,13 @@ package org.apache.shiro.crypto.hash; import org.apache.shiro.lang.util.ByteSource; +import org.apache.shiro.lang.util.SimpleByteSource; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import static java.util.Objects.requireNonNull; /** * A {@code HashRequest} is composed of data that will be used by a {@link HashService} to compute a hash (aka @@ -49,19 +56,7 @@ public interface HashRequest { * @return a salt to be used by the {@link HashService} during hash computation, or {@code null} if no salt is * provided as part of the request. */ - ByteSource getSalt(); - - /** - * Returns the number of requested hash iterations to be performed when computing the final {@code Hash} result. - * A non-positive (0 or less) indicates that the {@code HashService}'s default iteration configuration should - * be used. A positive value overrides the {@code HashService}'s configuration for a single request. - *

    - * Note that a {@code HashService} is free to ignore this number if it determines the number is not sufficient - * to meet a desired level of security. - * - * @return the number of requested hash iterations to be performed when computing the final {@code Hash} result. - */ - int getIterations(); + Optional getSalt(); /** * Returns the name of the hash algorithm the {@code HashService} should use when computing the {@link Hash}, or @@ -72,9 +67,26 @@ public interface HashRequest { * sufficient to meet a desired level of security. * * @return the name of the hash algorithm the {@code HashService} should use when computing the {@link Hash}, or - * {@code null} if the default algorithm configuration of the {@code HashService} should be used. + * {@code null} if the default algorithm configuration of the {@code HashService} should be used. + */ + Optional getAlgorithmName(); + + /** + * Returns various parameters for the requested hash. + * + *

    If the map is empty for a specific parameter, the implementation must select the default.

    + * + *

    Implementations should provide a nested {@code .Parameters} class with {@code public static final String}s + * for convenience.

    + * + *

    Example parameters the number of requested hash iterations (does not apply to bcrypt), + * memory and cpu constrains, etc. + * Please find their specific names in the implementation’s nested {@code .Parameters} class.

    + * + * @return the parameters for the requested hash to be used when computing the final {@code Hash} result. + * @throws NullPointerException if any of the values is {@code null}. */ - String getAlgorithmName(); + Map getParameters(); /** * A Builder class representing the Builder design pattern for constructing {@link HashRequest} instances. @@ -85,15 +97,14 @@ public interface HashRequest { public static class Builder { private ByteSource source; - private ByteSource salt; - private int iterations; + private ByteSource salt = SimpleByteSource.empty(); + private Map parameters = new ConcurrentHashMap<>(); private String algorithmName; /** * Default no-arg constructor. */ public Builder() { - this.iterations = 0; } /** @@ -170,24 +181,14 @@ public Builder setSalt(Object salt) throws IllegalArgumentException { return this; } - /** - * Sets the number of requested hash iterations to be performed when computing the final {@code Hash} result. - * Not calling this method or setting a non-positive value (0 or less) indicates that the {@code HashService}'s - * default iteration configuration should be used. A positive value overrides the {@code HashService}'s - * configuration for a single request. - *

    - * Note that a {@code HashService} is free to ignore this number if it determines the number is not sufficient - * to meet a desired level of security. You can always check the result - * {@code Hash} {@link Hash#getIterations() getIterations()} method to see what the actual - * number of iterations was, which may or may not match this request salt. - * - * @param iterations the number of requested hash iterations to be performed when computing the final - * {@code Hash} result. - * @return this {@code Builder} instance for method chaining. - * @see HashRequest#getIterations() - */ - public Builder setIterations(int iterations) { - this.iterations = iterations; + public Builder addParameter(String parameterName, Object parameterValue) { + this.parameters.put(parameterName, requireNonNull(parameterValue)); + return this; + } + + public Builder withParameters(Map parameters) { + this.parameters.clear(); + this.parameters.putAll(requireNonNull(parameters)); return this; } @@ -219,7 +220,7 @@ public Builder setAlgorithmName(String algorithmName) { * @return a {@link HashRequest} instance reflecting the specified configuration. */ public HashRequest build() { - return new SimpleHashRequest(this.algorithmName, this.source, this.salt, this.iterations); + return new SimpleHashRequest(this.algorithmName, this.source, this.salt, this.parameters); } } } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashSpi.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashSpi.java new file mode 100644 index 0000000000..de4f2cf65a --- /dev/null +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/HashSpi.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.shiro.crypto.hash; + +import java.util.Random; +import java.util.Set; + +/** + * Service Provider Interface for password hashing algorithms. + * + *

    Apache Shiro will load algorithm implementations based on the method {@link #getImplementedAlgorithms()}. + * Loaded providers are expected to return a suitable hash implementation.

    + * + *

    Modern kdf-based hash implementations can extend the {@link AbstractCryptHash} class.

    + * + * @since 2.0 + */ +public interface HashSpi { + + /** + * A list of algorithms recognized by this implementation. + * + *

    Example values are {@code argon2id} and {@code argon2i} for the Argon2 service provider and + * {@code 2y} and {@code 2a} for the BCrypt service provider.

    + * + * @return a set of recognized algorithms. + */ + Set getImplementedAlgorithms(); + + /** + * Creates a Hash instance from the given format string recognized by this provider. + * + *

    There is no global format which this provider must accept. Each provider can define their own + * format, but they are usually based on the {@code crypt(3)} formats used in {@code /etc/shadow} files.

    + * + *

    Implementations should overwrite this javadoc to add examples of the accepted formats.

    + * + * @param format the format string to be parsed by this implementation. + * @return a class extending Hash. + */ + Hash fromString(String format); + + /** + * A factory class for the hash of the type {@code }. + * + *

    Implementations are highly encouraged to use the given random parameter as + * source of random bytes (e.g. for seeds).

    + * + * @param random a source of {@link Random}, usually {@code SecureRandom}. + * @return a factory class for creating instances of {@code }. + */ + HashFactory newHashFactory(Random random); + + interface HashFactory { + + /** + * Generates a hash from the given hash request. + * + *

    If the hash requests’ optional parameters are not set, the {@link HashFactory} implementation + * should use default parameters where applicable.

    + *

    If the hash requests’ salt is missing or empty, the implementation should create a salt + * with a default size.

    + * @param hashRequest the request to build a Hash from. + * @return a generated Hash according to the specs. + * @throws IllegalArgumentException if any of the parameters is outside of valid boundaries (algorithm-specific) + * or if the given algorithm is not applicable for this {@link HashFactory}. + */ + Hash generate(HashRequest hashRequest); + } +} diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Md2Hash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Md2Hash.java deleted file mode 100644 index dbfb9cb3db..0000000000 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Md2Hash.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.shiro.crypto.hash; - -import org.apache.shiro.lang.codec.Base64; -import org.apache.shiro.lang.codec.Hex; - - -/** - * Generates an MD2 Hash (RFC 1319) from a given input source with an optional salt and - * hash iterations. - *

    - * See the {@link SimpleHash SimpleHash} parent class JavaDoc for a detailed explanation of Hashing - * techniques and how the overloaded constructors function. - * - * @since 0.9 - */ -public class Md2Hash extends SimpleHash { - - public static final String ALGORITHM_NAME = "MD2"; - - public Md2Hash() { - super(ALGORITHM_NAME); - } - - public Md2Hash(Object source) { - super(ALGORITHM_NAME, source); - } - - public Md2Hash(Object source, Object salt) { - super(ALGORITHM_NAME, source, salt); - } - - public Md2Hash(Object source, Object salt, int hashIterations) { - super(ALGORITHM_NAME, source, salt, hashIterations); - } - - public static Md2Hash fromHexString(String hex) { - Md2Hash hash = new Md2Hash(); - hash.setBytes(Hex.decode(hex)); - return hash; - } - - public static Md2Hash fromBase64String(String base64) { - Md2Hash hash = new Md2Hash(); - hash.setBytes(Base64.decode(base64)); - return hash; - } -} diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Md5Hash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Md5Hash.java deleted file mode 100644 index a83740a642..0000000000 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Md5Hash.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.shiro.crypto.hash; - -import org.apache.shiro.lang.codec.Base64; -import org.apache.shiro.lang.codec.Hex; - -/** - * Generates an MD5 Hash (RFC 1321) from a given input source with an optional salt and - * hash iterations. - *

    - * See the {@link SimpleHash SimpleHash} parent class JavaDoc for a detailed explanation of Hashing - * techniques and how the overloaded constructors function. - * - * @since 0.9 - */ -public class Md5Hash extends SimpleHash { - - //TODO - complete JavaDoc - - public static final String ALGORITHM_NAME = "MD5"; - - public Md5Hash() { - super(ALGORITHM_NAME); - } - - public Md5Hash(Object source) { - super(ALGORITHM_NAME, source); - } - - public Md5Hash(Object source, Object salt) { - super(ALGORITHM_NAME, source, salt); - } - - public Md5Hash(Object source, Object salt, int hashIterations) { - super(ALGORITHM_NAME, source, salt, hashIterations); - } - - public static Md5Hash fromHexString(String hex) { - Md5Hash hash = new Md5Hash(); - hash.setBytes(Hex.decode(hex)); - return hash; - } - - public static Md5Hash fromBase64String(String base64) { - Md5Hash hash = new Md5Hash(); - hash.setBytes(Base64.decode(base64)); - return hash; - } -} diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Sha1Hash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Sha1Hash.java deleted file mode 100644 index e844b70408..0000000000 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/Sha1Hash.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.shiro.crypto.hash; - -import org.apache.shiro.lang.codec.Base64; -import org.apache.shiro.lang.codec.Hex; - - -/** - * Generates an SHA-1 Hash (Secure Hash Standard, NIST FIPS 180-1) from a given input source with an - * optional salt and hash iterations. - *

    - * See the {@link SimpleHash SimpleHash} parent class JavaDoc for a detailed explanation of Hashing - * techniques and how the overloaded constructors function. - * - * @since 0.9 - */ -public class Sha1Hash extends SimpleHash { - - //TODO - complete JavaDoc - - public static final String ALGORITHM_NAME = "SHA-1"; - - public Sha1Hash() { - super(ALGORITHM_NAME); - } - - public Sha1Hash(Object source) { - super(ALGORITHM_NAME, source); - } - - public Sha1Hash(Object source, Object salt) { - super(ALGORITHM_NAME, source, salt); - } - - public Sha1Hash(Object source, Object salt, int hashIterations) { - super(ALGORITHM_NAME, source, salt, hashIterations); - } - - public static Sha1Hash fromHexString(String hex) { - Sha1Hash hash = new Sha1Hash(); - hash.setBytes(Hex.decode(hex)); - return hash; - } - - public static Sha1Hash fromBase64String(String base64) { - Sha1Hash hash = new Sha1Hash(); - hash.setBytes(Base64.decode(base64)); - return hash; - } -} diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHash.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHash.java index 8c1fb6e939..eb58a895ef 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHash.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHash.java @@ -18,17 +18,22 @@ */ package org.apache.shiro.crypto.hash; +import org.apache.shiro.crypto.UnknownAlgorithmException; import org.apache.shiro.lang.codec.Base64; import org.apache.shiro.lang.codec.CodecException; import org.apache.shiro.lang.codec.Hex; -import org.apache.shiro.crypto.UnknownAlgorithmException; import org.apache.shiro.lang.util.ByteSource; +import org.apache.shiro.lang.util.SimpleByteSource; import org.apache.shiro.lang.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import static java.util.Objects.requireNonNull; + /** * A {@code Hash} implementation that allows any {@link java.security.MessageDigest MessageDigest} algorithm name to * be used. This class is a less type-safe variant than the other {@code AbstractHash} subclasses @@ -43,6 +48,9 @@ public class SimpleHash extends AbstractHash { private static final int DEFAULT_ITERATIONS = 1; + private static final long serialVersionUID = -6689895264902387303L; + + private static final Logger LOG = LoggerFactory.getLogger(SimpleHash.class); /** * The {@link java.security.MessageDigest MessageDigest} algorithm name to use when performing the hash. @@ -114,7 +122,7 @@ public SimpleHash(String algorithmName) { */ public SimpleHash(String algorithmName, Object source) throws CodecException, UnknownAlgorithmException { //noinspection NullableProblems - this(algorithmName, source, null, DEFAULT_ITERATIONS); + this(algorithmName, source, SimpleByteSource.empty(), DEFAULT_ITERATIONS); } /** @@ -139,6 +147,28 @@ public SimpleHash(String algorithmName, Object source, Object salt) throws Codec this(algorithmName, source, salt, DEFAULT_ITERATIONS); } + /** + * Creates an {@code algorithmName}-specific hash of the specified {@code source} using the given {@code salt} + * using a single hash iteration. + *

    + * It is a convenience constructor that merely executes this( algorithmName, source, salt, 1);. + *

    + * Please see the + * {@link #SimpleHash(String algorithmName, Object source, Object salt, int numIterations) SimpleHashHash(algorithmName, Object,Object,int)} + * constructor for the types of Objects that may be passed into this constructor, as well as how to support further + * types. + * + * @param algorithmName the {@link java.security.MessageDigest MessageDigest} algorithm name to use when + * performing the hash. + * @param source the source object to be hashed. + * @param hashIterations the number of times the {@code source} argument hashed for attack resiliency. + * @throws CodecException if either constructor argument cannot be converted into a byte array. + * @throws UnknownAlgorithmException if the {@code algorithmName} is not available. + */ + public SimpleHash(String algorithmName, Object source, int hashIterations) throws CodecException, UnknownAlgorithmException { + this(algorithmName, source, SimpleByteSource.empty(), hashIterations); + } + /** * Creates an {@code algorithmName}-specific hash of the specified {@code source} using the given * {@code salt} a total of {@code hashIterations} times. @@ -169,11 +199,8 @@ public SimpleHash(String algorithmName, Object source, Object salt, int hashIter } this.algorithmName = algorithmName; this.iterations = Math.max(DEFAULT_ITERATIONS, hashIterations); - ByteSource saltBytes = null; - if (salt != null) { - saltBytes = convertSaltToBytes(salt); - this.salt = saltBytes; - } + ByteSource saltBytes = convertSaltToBytes(salt); + this.salt = saltBytes; ByteSource sourceBytes = convertSourceToBytes(source); hash(sourceBytes, saltBytes, hashIterations); } @@ -209,23 +236,20 @@ protected ByteSource convertSaltToBytes(Object salt) { /** * Converts a given object into a {@code ByteSource} instance. Assumes the object can be converted to bytes. * - * @param o the Object to convert into a {@code ByteSource} instance. + * @param object the Object to convert into a {@code ByteSource} instance. * @return the {@code ByteSource} representation of the specified object's bytes. * @since 1.2 */ - protected ByteSource toByteSource(Object o) { - if (o == null) { - return null; + protected ByteSource toByteSource(Object object) { + if (object instanceof ByteSource) { + return (ByteSource) object; } - if (o instanceof ByteSource) { - return (ByteSource) o; - } - byte[] bytes = toBytes(o); + byte[] bytes = toBytes(object); return ByteSource.Util.bytes(bytes); } private void hash(ByteSource source, ByteSource salt, int hashIterations) throws CodecException, UnknownAlgorithmException { - byte[] saltBytes = salt != null ? salt.getBytes() : null; + byte[] saltBytes = requireNonNull(salt).getBytes(); byte[] hashedBytes = hash(source.getBytes(), saltBytes, hashIterations); setBytes(hashedBytes); } @@ -235,18 +259,34 @@ private void hash(ByteSource source, ByteSource salt, int hashIterations) throws * * @return the {@link java.security.MessageDigest MessageDigest} algorithm name to use when performing the hash. */ + @Override public String getAlgorithmName() { return this.algorithmName; } + @Override public ByteSource getSalt() { return this.salt; } + @Override public int getIterations() { return this.iterations; } + @Override + public boolean matchesPassword(ByteSource plaintextBytes) { + try { + SimpleHash otherHash = new SimpleHash(this.getAlgorithmName(), plaintextBytes, this.getSalt(), this.getIterations()); + return this.equals(otherHash); + } catch (IllegalArgumentException illegalArgumentException) { + // cannot recreate hash. Do not log password. + LOG.warn("Cannot recreate a hash using the same parameters.", illegalArgumentException); + return false; + } + } + + @Override public byte[] getBytes() { return this.bytes; } @@ -259,6 +299,7 @@ public byte[] getBytes() { * * @param alreadyHashedBytes the raw already-hashed bytes to store in this instance. */ + @Override public void setBytes(byte[] alreadyHashedBytes) { this.bytes = alreadyHashedBytes; this.hexEncoded = null; @@ -298,6 +339,7 @@ public void setSalt(ByteSource salt) { * @return the MessageDigest object for the specified {@code algorithm}. * @throws UnknownAlgorithmException if the specified algorithm name is not available. */ + @Override protected MessageDigest getDigest(String algorithmName) throws UnknownAlgorithmException { try { return MessageDigest.getInstance(algorithmName); @@ -314,6 +356,7 @@ protected MessageDigest getDigest(String algorithmName) throws UnknownAlgorithmE * @return the hashed bytes. * @throws UnknownAlgorithmException if the configured {@link #getAlgorithmName() algorithmName} is not available. */ + @Override protected byte[] hash(byte[] bytes) throws UnknownAlgorithmException { return hash(bytes, null, DEFAULT_ITERATIONS); } @@ -326,6 +369,7 @@ protected byte[] hash(byte[] bytes) throws UnknownAlgorithmException { * @return the hashed bytes * @throws UnknownAlgorithmException if the configured {@link #getAlgorithmName() algorithmName} is not available. */ + @Override protected byte[] hash(byte[] bytes, byte[] salt) throws UnknownAlgorithmException { return hash(bytes, salt, DEFAULT_ITERATIONS); } @@ -339,9 +383,10 @@ protected byte[] hash(byte[] bytes, byte[] salt) throws UnknownAlgorithmExceptio * @return the hashed bytes. * @throws UnknownAlgorithmException if the {@link #getAlgorithmName() algorithmName} is not available. */ + @Override protected byte[] hash(byte[] bytes, byte[] salt, int hashIterations) throws UnknownAlgorithmException { MessageDigest digest = getDigest(getAlgorithmName()); - if (salt != null) { + if (salt.length != 0) { digest.reset(); digest.update(salt); } @@ -355,6 +400,7 @@ protected byte[] hash(byte[] bytes, byte[] salt, int hashIterations) throws Unkn return hashed; } + @Override public boolean isEmpty() { return this.bytes == null || this.bytes.length == 0; } @@ -368,6 +414,7 @@ public boolean isEmpty() { * * @return a hex-encoded string of the underlying {@link #getBytes byte array}. */ + @Override public String toHex() { if (this.hexEncoded == null) { this.hexEncoded = Hex.encodeToString(getBytes()); @@ -384,6 +431,7 @@ public String toHex() { * * @return a Base64-encoded string of the underlying {@link #getBytes byte array}. */ + @Override public String toBase64() { if (this.base64Encoded == null) { //cache result in case this method is called multiple times. @@ -397,6 +445,7 @@ public String toBase64() { * * @return the {@link #toHex() toHex()} value. */ + @Override public String toString() { return toHex(); } @@ -409,6 +458,7 @@ public String toString() { * @return {@code true} if the specified object is a Hash and its {@link #getBytes byte array} is identical to * this Hash's byte array, {@code false} otherwise. */ + @Override public boolean equals(Object o) { if (o instanceof Hash) { Hash other = (Hash) o; @@ -422,6 +472,7 @@ public boolean equals(Object o) { * * @return toHex().hashCode() */ + @Override public int hashCode() { if (this.bytes == null || this.bytes.length == 0) { return 0; diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashProvider.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashProvider.java new file mode 100644 index 0000000000..5b4a44df2c --- /dev/null +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashProvider.java @@ -0,0 +1,219 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.shiro.crypto.hash; + +import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat; +import org.apache.shiro.lang.util.ByteSource; +import org.apache.shiro.lang.util.SimpleByteSource; + +import java.util.Arrays; +import java.util.Base64; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Random; +import java.util.Set; + +import static java.util.Collections.unmodifiableSet; +import static java.util.stream.Collectors.toSet; + +/** + * Creates a hash provider for salt (+pepper) and Hash-based KDFs, i.e. where the algorithm name + * is a SHA algorithm or similar. + * @since 2.0 + */ +public class SimpleHashProvider implements HashSpi { + + private static final Set IMPLEMENTED_ALGORITHMS = Arrays.stream(new String[]{ + Sha256Hash.ALGORITHM_NAME, + Sha384Hash.ALGORITHM_NAME, + Sha512Hash.ALGORITHM_NAME + }) + .collect(toSet()); + + @Override + public Set getImplementedAlgorithms() { + return unmodifiableSet(IMPLEMENTED_ALGORITHMS); + } + + @Override + public SimpleHash fromString(String format) { + Hash hash = new Shiro1CryptFormat().parse(format); + + if (!(hash instanceof SimpleHash)) { + throw new IllegalArgumentException("formatted string was not a simple hash: " + format); + } + + return (SimpleHash) hash; + } + + @Override + public HashFactory newHashFactory(Random random) { + return new SimpleHashFactory(random); + } + + static class SimpleHashFactory implements HashSpi.HashFactory { + + private final Random random; + + public SimpleHashFactory(Random random) { + this.random = random; + } + + @Override + public SimpleHash generate(HashRequest hashRequest) { + String algorithmName = hashRequest.getAlgorithmName().orElse(Parameters.DEFAULT_ALGORITHM); + ByteSource source = hashRequest.getSource(); + final int iterations = getIterations(hashRequest); + + final ByteSource publicSalt = getPublicSalt(hashRequest); + final /*nullable*/ ByteSource secretSalt = getSecretSalt(hashRequest); + final ByteSource salt = combine(secretSalt, publicSalt); + + return createSimpleHash(algorithmName, source, iterations, publicSalt, salt); + } + + /** + * Returns the public salt that should be used to compute a hash based on the specified request. + *

    + * This implementation functions as follows: + *

      + *
    1. If the request salt is not null and non-empty, this will be used, return it.
    2. + *
    3. If the request salt is null or empty: + *
      1. create a new 16-byte salt.
      + *
    4. + *
    + * + * @param request request the request to process + * @return the public salt that should be used to compute a hash based on the specified request or + * {@code null} if no public salt should be used. + */ + protected ByteSource getPublicSalt(HashRequest request) { + Optional publicSalt = request.getSalt(); + + if (publicSalt.isPresent() && !publicSalt.orElseThrow(NoSuchElementException::new).isEmpty()) { + //a public salt was explicitly requested to be used - go ahead and use it: + return publicSalt.orElseThrow(NoSuchElementException::new); + } + + // generate salt if absent from the request. + byte[] ps = new byte[16]; + random.nextBytes(ps); + + return new SimpleByteSource(ps); + } + + private ByteSource getSecretSalt(HashRequest request) { + Optional secretSalt = Optional.ofNullable(request.getParameters().get(Parameters.PARAMETER_SECRET_SALT)); + + return secretSalt + .map(salt -> (String) salt) + .map(salt -> Base64.getDecoder().decode(salt)) + .map(SimpleByteSource::new) + .orElse(null); + } + + private SimpleHash createSimpleHash(String algorithmName, ByteSource source, int iterations, ByteSource publicSalt, ByteSource salt) { + Hash computed = new SimpleHash(algorithmName, source, salt, iterations); + + SimpleHash result = new SimpleHash(algorithmName); + result.setBytes(computed.getBytes()); + result.setIterations(iterations); + //Only expose the public salt - not the real/combined salt that might have been used: + result.setSalt(publicSalt); + + return result; + } + + protected int getIterations(HashRequest request) { + Object parameterIterations = request.getParameters().getOrDefault(Parameters.PARAMETER_ITERATIONS, 0); + + if (!(parameterIterations instanceof Integer)) { + return Parameters.DEFAULT_ITERATIONS; + } + + final int iterations = Math.max(0, (Integer) parameterIterations); + + if (iterations < 1) { + return Parameters.DEFAULT_ITERATIONS; + } + + return iterations; + } + + /** + * Combines the specified 'private' salt bytes with the specified additional extra bytes to use as the + * total salt during hash computation. {@code privateSaltBytes} will be {@code null} }if no private salt has been + * configured. + * + * @param privateSalt the (possibly {@code null}) 'private' salt to combine with the specified extra bytes + * @param publicSalt the extra bytes to use in addition to the given private salt. + * @return a combination of the specified private salt bytes and extra bytes that will be used as the total + * salt during hash computation. + */ + protected ByteSource combine(ByteSource privateSalt, ByteSource publicSalt) { + + // optional 'pepper' + byte[] privateSaltBytes = privateSalt != null ? privateSalt.getBytes() : null; + int privateSaltLength = privateSaltBytes != null ? privateSaltBytes.length : 0; + + // salt must always be present. + byte[] publicSaltBytes = publicSalt.getBytes(); + int extraBytesLength = publicSaltBytes.length; + + int length = privateSaltLength + extraBytesLength; + + if (length <= 0) { + return SimpleByteSource.empty(); + } + + byte[] combined = new byte[length]; + + int i = 0; + for (int j = 0; j < privateSaltLength; j++) { + combined[i++] = privateSaltBytes[j]; + } + for (int j = 0; j < extraBytesLength; j++) { + combined[i++] = publicSaltBytes[j]; + } + + return ByteSource.Util.bytes(combined); + } + } + + static final class Parameters { + public static final String PARAMETER_ITERATIONS = "SimpleHash.iterations"; + + /** + * A secret part added to the salt. Sometimes also referred to as {@literal "Pepper"}. + * + *

    For more information, see Pepper (cryptography) on Wikipedia.

    + */ + public static final String PARAMETER_SECRET_SALT = "SimpleHash.secretSalt"; + + public static final String DEFAULT_ALGORITHM = "SHA-512"; + + public static final int DEFAULT_ITERATIONS = 50_000; + + + private Parameters() { + // util class + } + } +} diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashRequest.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashRequest.java index 5423256739..ffd2989d28 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashRequest.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/SimpleHashRequest.java @@ -20,6 +20,13 @@ import org.apache.shiro.lang.util.ByteSource; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import static java.util.Collections.unmodifiableMap; +import static java.util.Objects.requireNonNull; + /** * Simple implementation of {@link HashRequest} that can be used when interacting with a {@link HashService}. * @@ -29,46 +36,46 @@ public class SimpleHashRequest implements HashRequest { private final ByteSource source; //cannot be null - this is the source to hash. private final ByteSource salt; //null = no salt specified - private final int iterations; //0 = not specified by the requestor; let the HashService decide. private final String algorithmName; //null = let the HashService decide. + private final Map parameters = new ConcurrentHashMap<>(); /** * Creates a new SimpleHashRequest instance. * * @param algorithmName the name of the hash algorithm to use. This is often null as the - * {@link HashService} implementation is usually configured with an appropriate algorithm name, but this - * can be non-null if the hash service's algorithm should be overridden with a specific one for the duration - * of the request. - * - * @param source the source to be hashed - * @param salt any public salt which should be used when computing the hash - * @param iterations the number of hash iterations to execute. Zero (0) indicates no iterations were specified - * for the request, at which point the number of iterations is decided by the {@code HashService} - * @throws NullPointerException if {@code source} is null or empty. + * {@link HashService} implementation is usually configured with an appropriate algorithm name, but this + * can be non-null if the hash service's algorithm should be overridden with a specific one for the duration + * of the request. + * @param source the source to be hashed + * @param salt any public salt which should be used when computing the hash + * @param parameters e.g. the number of hash iterations to execute or other parameters. + * @throws NullPointerException if {@code source} is null or empty or {@code parameters} is {@code null}. */ - public SimpleHashRequest(String algorithmName, ByteSource source, ByteSource salt, int iterations) { - if (source == null) { - throw new NullPointerException("source argument cannot be null"); - } - this.source = source; + public SimpleHashRequest(String algorithmName, ByteSource source, ByteSource salt, Map parameters) { + this.source = requireNonNull(source); this.salt = salt; this.algorithmName = algorithmName; - this.iterations = Math.max(0, iterations); + this.parameters.putAll(requireNonNull(parameters)); } + @Override public ByteSource getSource() { return this.source; } - public ByteSource getSalt() { - return this.salt; + @Override + public Optional getSalt() { + return Optional.ofNullable(this.salt); } - public int getIterations() { - return iterations; + + @Override + public Optional getAlgorithmName() { + return Optional.ofNullable(algorithmName); } - public String getAlgorithmName() { - return algorithmName; + @Override + public Map getParameters() { + return unmodifiableMap(this.parameters); } } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Base64Format.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Base64Format.java index 78742c0c5c..35b3394b3c 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Base64Format.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Base64Format.java @@ -20,22 +20,28 @@ import org.apache.shiro.crypto.hash.Hash; +import static java.util.Objects.requireNonNull; + /** * {@code HashFormat} that outputs only the hash's digest bytes in Base64 format. It does not print out * anything else (salt, iterations, etc). This implementation is mostly provided as a convenience for * command-line hashing. * * @since 1.2 + * @deprecated will throw exceptions in 2.1.0, to be removed in 2.2.0 */ +@Deprecated public class Base64Format implements HashFormat { /** - * Returns {@code hash != null ? hash.toBase64() : null}. + * Returns {@code hash.toBase64()}. * * @param hash the hash instance to format into a String. - * @return {@code hash != null ? hash.toBase64() : null}. + * @return {@code hash.toBase64()}. + * @throws NullPointerException if hash is {@code null}. */ - public String format(Hash hash) { - return hash != null ? hash.toBase64() : null; + @Override + public String format(final Hash hash) { + return requireNonNull(hash).toBase64(); } } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactory.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactory.java index 34553d9b16..ae09b13bce 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactory.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactory.java @@ -111,13 +111,14 @@ public void setSearchPackages(Set searchPackages) { this.searchPackages = searchPackages; } + @Override public HashFormat getInstance(String in) { if (in == null) { return null; } HashFormat hashFormat = null; - Class clazz = null; + Class clazz = null; //NOTE: this code block occurs BEFORE calling getHashFormatClass(in) on purpose as a performance //optimization. If the input arg is an MCF-formatted string, there will be many unnecessary ClassLoader @@ -128,7 +129,7 @@ public HashFormat getInstance(String in) { String test = in.substring(ModularCryptFormat.TOKEN_DELIMITER.length()); String[] tokens = test.split("\\" + ModularCryptFormat.TOKEN_DELIMITER); //the MCF ID is always the first token in the delimited string: - String possibleMcfId = (tokens != null && tokens.length > 0) ? tokens[0] : null; + String possibleMcfId = tokens.length > 0 ? tokens[0] : null; if (possibleMcfId != null) { //found a possible MCF ID - test it using our heuristics to see if we can find a corresponding class: clazz = getHashFormatClass(possibleMcfId); diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HashFormat.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HashFormat.java index c65ae78b5f..29d8535f95 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HashFormat.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HashFormat.java @@ -24,13 +24,11 @@ * A {@code HashFormat} is able to format a {@link Hash} instance into a well-defined formatted String. *

    * Note that not all HashFormat algorithms are reversible. That is, they can't be parsed and reconstituted to the - * original Hash instance. The traditional - * Unix crypt(3) is one such format. + * original Hash instance. *

    * The formats that are reversible however will be represented as {@link ParsableHashFormat} instances. * * @see ParsableHashFormat - * * @since 1.2 */ public interface HashFormat { @@ -40,6 +38,7 @@ public interface HashFormat { * * @param hash the hash instance to format into a String. * @return a formatted string representing the specified Hash instance. + * @throws NullPointerException if given parameter hash is {@code null}. */ String format(Hash hash); } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HexFormat.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HexFormat.java index 5730ac990c..2dfb802819 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HexFormat.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/HexFormat.java @@ -26,16 +26,20 @@ * command-line hashing. * * @since 1.2 + * @deprecated will throw exceptions in 2.1.0, to be removed in 2.2.0 */ +@Deprecated public class HexFormat implements HashFormat { /** - * Returns {@code hash != null ? hash.toHex() : null}. + * Returns {@code hash.toHex()}. * * @param hash the hash instance to format into a String. - * @return {@code hash != null ? hash.toHex() : null}. + * @return {@code hash.toHex()}. + * @throws NullPointerException if given parameter hash is {@code null}. */ - public String format(Hash hash) { - return hash != null ? hash.toHex() : null; + @Override + public String format(final Hash hash) { + return hash.toHex(); } } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ProvidedHashFormat.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ProvidedHashFormat.java index 3813123217..9ed5246d86 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ProvidedHashFormat.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/ProvidedHashFormat.java @@ -40,11 +40,16 @@ public enum ProvidedHashFormat { /** * Value representing the {@link Shiro1CryptFormat} implementation. */ - SHIRO1(Shiro1CryptFormat.class); + SHIRO1(Shiro1CryptFormat.class), + + /** + * Value representing the {@link Shiro2CryptFormat} implementation. + */ + SHIRO2(Shiro2CryptFormat.class); private final Class clazz; - private ProvidedHashFormat(Class clazz) { + ProvidedHashFormat(final Class clazz) { this.clazz = clazz; } @@ -52,7 +57,7 @@ Class getHashFormatClass() { return this.clazz; } - public static ProvidedHashFormat byId(String id) { + public static ProvidedHashFormat byId(final String id) { if (id == null) { return null; } @@ -60,7 +65,7 @@ public static ProvidedHashFormat byId(String id) { // Use English Locale, some Locales handle uppercase/lower differently. i.e. Turkish and upper case 'i' // is not 'I'. And 'SHIRO1' would be 'SHİRO1' return valueOf(id.toUpperCase(Locale.ENGLISH)); - } catch (IllegalArgumentException ignored) { + } catch (final IllegalArgumentException ignored) { return null; } } diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Shiro1CryptFormat.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Shiro1CryptFormat.java index 24966ea4db..1428f3abd9 100644 --- a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Shiro1CryptFormat.java +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Shiro1CryptFormat.java @@ -18,9 +18,10 @@ */ package org.apache.shiro.crypto.hash.format; -import org.apache.shiro.lang.codec.Base64; import org.apache.shiro.crypto.hash.Hash; import org.apache.shiro.crypto.hash.SimpleHash; +import org.apache.shiro.crypto.hash.SimpleHashProvider; +import org.apache.shiro.lang.codec.Base64; import org.apache.shiro.lang.util.ByteSource; import org.apache.shiro.lang.util.StringUtils; @@ -93,11 +94,13 @@ public class Shiro1CryptFormat implements ModularCryptFormat, ParsableHashFormat public Shiro1CryptFormat() { } + @Override public String getId() { return ID; } - public String format(Hash hash) { + @Override + public String format(final Hash hash) { if (hash == null) { return null; } @@ -117,7 +120,8 @@ public String format(Hash hash) { return sb.toString(); } - public Hash parse(String formatted) { + @Override + public Hash parse(final String formatted) { if (formatted == null) { return null; } @@ -130,13 +134,17 @@ public Hash parse(String formatted) { String suffix = formatted.substring(MCF_PREFIX.length()); String[] parts = suffix.split("\\$"); + final String algorithmName = parts[0]; + if (!new SimpleHashProvider().getImplementedAlgorithms().contains(algorithmName)) { + throw new UnsupportedOperationException("Algorithm " + algorithmName + " is not supported in shiro1 format."); + } + //last part is always the digest/checksum, Base64-encoded: - int i = parts.length-1; + int i = parts.length - 1; String digestBase64 = parts[i--]; //second-to-last part is always the salt, Base64-encoded: String saltBase64 = parts[i--]; String iterationsString = parts[i--]; - String algorithmName = parts[i]; byte[] digest = Base64.decode(digestBase64); ByteSource salt = null; diff --git a/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Shiro2CryptFormat.java b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Shiro2CryptFormat.java new file mode 100644 index 0000000000..7cc3ff1e38 --- /dev/null +++ b/crypto/hash/src/main/java/org/apache/shiro/crypto/hash/format/Shiro2CryptFormat.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.shiro.crypto.hash.format; + +import org.apache.shiro.crypto.hash.AbstractCryptHash; +import org.apache.shiro.crypto.hash.Hash; +import org.apache.shiro.crypto.hash.HashProvider; +import org.apache.shiro.crypto.hash.HashSpi; +import org.apache.shiro.crypto.hash.SimpleHash; + +import static java.util.Objects.requireNonNull; + +/** + * The {@code Shiro1CryptFormat} is a fully reversible + * Modular Crypt Format (MCF). It is based + * on the posix format for storing KDF-hashed passwords in {@code /etc/shadow} files on linux and unix-alike systems. + *

    Format

    + *

    Hash instances formatted with this implementation will result in a String with the following dollar-sign ($) + * delimited format:

    + *
    + * $mcfFormatId$algorithmName$algorithm-specific-data.
    + * 
    + *

    Each token is defined as follows:

    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    PositionTokenDescriptionRequired?
    1{@code mcfFormatId}The Modular Crypt Format identifier for this implementation, equal to {@code shiro2}. + * ( This implies that all {@code shiro2} MCF-formatted strings will always begin with the prefix + * {@code $shiro2$} ).true
    2{@code algorithmName}The name of the hash algorithm used to perform the hash. Either a hash class exists, or + * otherwise a {@link UnsupportedOperationException} will be thrown. + * true
    3{@code algorithm-specific-data}In contrast to the previous {@code shiro1} format, the shiro2 format does not make any assumptions + * about how an algorithm stores its data. Therefore, everything beyond the first token is handled over + * to the Hash implementation.
    + * + * @see ModularCryptFormat + * @see ParsableHashFormat + * @since 2.0 + */ +public class Shiro2CryptFormat implements ModularCryptFormat, ParsableHashFormat { + + /** + * Identifier for the shiro2 crypt format. + */ + public static final String ID = "shiro2"; + /** + * Enclosed identifier of the shiro2 crypt format. + */ + public static final String MCF_PREFIX = TOKEN_DELIMITER + ID + TOKEN_DELIMITER; + + public Shiro2CryptFormat() { + } + + @Override + public String getId() { + return ID; + } + + /** + * Converts a Hash-extending class to a string understood by the hash class. Usually this string will follow + * posix standards for passwords stored in {@code /etc/passwd}. + * + *

    This method should only delegate to the corresponding formatter and prepend {@code $shiro2$}.

    + * + * @param hash the hash instance to format into a String. + * @return a string representing the hash. + */ + @Override + public String format(final Hash hash) { + requireNonNull(hash, "hash in Shiro2CryptFormat.format(Hash hash)"); + + // backwards compatibility until Shiro 2.1.0. + if (hash instanceof SimpleHash) { + return new Shiro1CryptFormat().format(hash); + } + + if (!(hash instanceof AbstractCryptHash)) { + throw new UnsupportedOperationException("Shiro2CryptFormat can only format classes extending AbstractCryptHash."); + } + + AbstractCryptHash cryptHash = (AbstractCryptHash) hash; + return TOKEN_DELIMITER + ID + cryptHash.formatToCryptString(); + } + + @Override + public Hash parse(final String formatted) { + requireNonNull(formatted, "formatted in Shiro2CryptFormat.parse(String formatted)"); + + // backwards compatibility until Shiro 2.1.0. + if (formatted.startsWith(Shiro1CryptFormat.MCF_PREFIX)) { + return new Shiro1CryptFormat().parse(formatted); + } + + if (!formatted.startsWith(MCF_PREFIX)) { + final String msg = "The argument is not a valid '" + ID + "' formatted hash."; + throw new IllegalArgumentException(msg); + } + + final String suffix = formatted.substring(MCF_PREFIX.length()); + final String[] parts = suffix.split("\\$"); + final String algorithmName = parts[0]; + + HashSpi kdfHash = HashProvider.getByAlgorithmName(algorithmName) + .orElseThrow(() -> new UnsupportedOperationException("Algorithm " + algorithmName + " is not implemented.")); + return kdfHash.fromString("$" + suffix); + } + +} diff --git a/crypto/hash/src/main/resources/META-INF/NOTICE b/crypto/hash/src/main/resources/META-INF/NOTICE index 9d26a95ffb..5976d79428 100644 --- a/crypto/hash/src/main/resources/META-INF/NOTICE +++ b/crypto/hash/src/main/resources/META-INF/NOTICE @@ -7,7 +7,7 @@ The Apache Software Foundation (http://www.apache.org/). The implementation for org.apache.shiro.util.SoftHashMap is based on initial ideas from Dr. Heinz Kabutz's publicly posted version available at http://www.javaspecialists.eu/archive/Issue015.html, -with continued modifications. +with continued modifications. Certain parts (StringUtils, IpAddressMatcher, etc.) of the source code for this product was copied for simplicity and to reduce diff --git a/crypto/hash/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi b/crypto/hash/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi new file mode 100644 index 0000000000..dc9d0d2317 --- /dev/null +++ b/crypto/hash/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +org.apache.shiro.crypto.hash.SimpleHashProvider diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/DefaultHashServiceTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/DefaultHashServiceTest.groovy index d021be272c..389ba3a140 100644 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/DefaultHashServiceTest.groovy +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/DefaultHashServiceTest.groovy @@ -18,12 +18,10 @@ */ package org.apache.shiro.crypto.hash -import org.apache.shiro.crypto.RandomNumberGenerator -import org.apache.shiro.crypto.SecureRandomNumberGenerator + import org.apache.shiro.lang.util.ByteSource import org.junit.Test -import static org.easymock.EasyMock.* import static org.junit.Assert.* /** @@ -35,54 +33,27 @@ class DefaultHashServiceTest { @Test void testNullRequest() { - assertNull createService().computeHash(null) + assertNull createSha256Service().computeHash(null) } @Test void testDifferentAlgorithmName() { - def service = new DefaultHashService(hashAlgorithmName: 'MD5') - def hash = hash(service, "test") - assertEquals 'MD5', hash.algorithmName - } + // given + def newAlgorithm = 'SHA-512' + def service = new DefaultHashService(defaultAlgorithmName: newAlgorithm) - @Test - void testDifferentIterations() { - def service = new DefaultHashService(hashIterations: 2) + // when def hash = hash(service, "test") - assertEquals 2, hash.iterations - } - - @Test - void testDifferentRandomNumberGenerator() { - def ByteSource randomBytes = new SecureRandomNumberGenerator().nextBytes() - def rng = createMock(RandomNumberGenerator) - expect(rng.nextBytes()).andReturn randomBytes - - replay rng - - def service = new DefaultHashService(randomNumberGenerator: rng, generatePublicSalt: true) - hash(service, "test") - - verify rng - } - - /** - * If 'generatePublicSalt' is true, 2 hashes of the same input source should be different. - */ - @Test - void testWithRandomlyGeneratedSalt() { - def service = new DefaultHashService(generatePublicSalt: true) - def first = hash(service, "password") - def second = hash(service, "password") - assertFalse first == second + // then + assertEquals newAlgorithm, hash.algorithmName } @Test void testRequestWithEmptySource() { def source = ByteSource.Util.bytes((byte[])null) def request = new HashRequest.Builder().setSource(source).build() - def service = createService() + def service = createSha256Service() assertNull service.computeHash(request) } @@ -92,7 +63,7 @@ class DefaultHashServiceTest { */ @Test void testOnlyRandomSaltHash() { - HashService service = createService(); + HashService service = createSha256Service(); Hash first = hash(service, "password"); Hash second = hash(service, "password2", first.salt); assertFalse first == second @@ -104,7 +75,7 @@ class DefaultHashServiceTest { */ @Test void testBothSaltsRandomness() { - HashService service = createServiceWithPrivateSalt(); + HashService service = createSha256Service(); Hash first = hash(service, "password"); Hash second = hash(service, "password"); assertFalse first == second @@ -117,7 +88,7 @@ class DefaultHashServiceTest { */ @Test void testBothSaltsReturn() { - HashService service = createServiceWithPrivateSalt(); + HashService service = createSha256Service(); Hash first = hash(service, "password"); Hash second = hash(service, "password", first.salt); assertEquals first, second @@ -129,24 +100,12 @@ class DefaultHashServiceTest { */ @Test void testBothSaltsHash() { - HashService service = createServiceWithPrivateSalt(); + HashService service = createSha256Service(); Hash first = hash(service, "password"); Hash second = hash(service, "password2", first.salt); assertFalse first == second } - /** - * Hash result is different if the base salt is added. - */ - @Test - public void testPrivateSaltChangesResult() { - HashService saltedService = createServiceWithPrivateSalt(); - HashService service = createService(); - Hash first = hashPredictable(saltedService, "password"); - Hash second = hashPredictable(service, "password"); - assertFalse first == second - } - protected Hash hash(HashService hashService, def source) { return hashService.computeHash(new HashRequest.Builder().setSource(source).build()); } @@ -155,19 +114,8 @@ class DefaultHashServiceTest { return hashService.computeHash(new HashRequest.Builder().setSource(source).setSalt(salt).build()); } - private Hash hashPredictable(HashService hashService, def source) { - byte[] salt = new byte[20]; - Arrays.fill(salt, (byte) 2); - return hashService.computeHash(new HashRequest.Builder().setSource(source).setSalt(salt).build()); - } - - private DefaultHashService createService() { - return new DefaultHashService(); + private static DefaultHashService createSha256Service() { + return new DefaultHashService(defaultAlgorithmName: 'SHA-256'); } - private DefaultHashService createServiceWithPrivateSalt() { - DefaultHashService defaultHashService = new DefaultHashService(); - defaultHashService.setPrivateSalt(new SecureRandomNumberGenerator().nextBytes()); - return defaultHashService; - } } diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/HashRequestBuilderTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/HashRequestBuilderTest.groovy index 527098cb30..323de38fa9 100644 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/HashRequestBuilderTest.groovy +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/HashRequestBuilderTest.groovy @@ -20,9 +20,10 @@ package org.apache.shiro.crypto.hash import org.apache.shiro.crypto.SecureRandomNumberGenerator import org.apache.shiro.lang.util.ByteSource -import org.junit.Test +import org.junit.jupiter.api.Test + +import static org.junit.jupiter.api.Assertions.* -import static org.junit.Assert.* /** * Unit tests for the {@link HashRequest.Builder} implementation @@ -33,16 +34,7 @@ class HashRequestBuilderTest { @Test void testNullSource() { - try { - new HashRequest.Builder().build() - fail "NullPointerException should be thrown" - } catch (NullPointerException expected) { - } - } - - @Test - void testDefault() { - assertEquals 0, new HashRequest.Builder().setSource("test").build().iterations + assertThrows NullPointerException, { new HashRequest.Builder().build() } } @Test @@ -50,15 +42,16 @@ class HashRequestBuilderTest { ByteSource source = ByteSource.Util.bytes("test") ByteSource salt = new SecureRandomNumberGenerator().nextBytes() def request = new HashRequest.Builder() - .setSource(source) - .setSalt(salt) - .setIterations(2) - .setAlgorithmName('MD5').build() + .setSource(source) + .setSalt(salt) + .addParameter(SimpleHashProvider.Parameters.PARAMETER_ITERATIONS, 2) + .setAlgorithmName('MD5') + .build() assertNotNull request assertEquals source, request.source - assertEquals salt, request.salt - assertEquals 2, request.iterations - assertEquals 'MD5', request.algorithmName + assertEquals salt, request.salt.orElse(null) + assertEquals 2, request.getParameters().get(SimpleHashProvider.Parameters.PARAMETER_ITERATIONS) + assertEquals 'MD5', request.algorithmName.orElse(null) } } diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/Base64FormatTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/Base64FormatTest.groovy index 75cb26622d..737eedcf30 100644 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/Base64FormatTest.groovy +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/Base64FormatTest.groovy @@ -19,9 +19,11 @@ package org.apache.shiro.crypto.hash.format import org.apache.shiro.crypto.hash.Hash -import org.apache.shiro.crypto.hash.Sha1Hash +import org.apache.shiro.crypto.hash.Sha512Hash import org.junit.Test -import static org.junit.Assert.* + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertThrows /** * Unit tests for the {@link Base64Format} implementation. @@ -32,7 +34,7 @@ class Base64FormatTest { @Test void testFormat() { - Hash hash = new Sha1Hash("hello"); + Hash hash = new Sha512Hash("hello"); Base64Format format = new Base64Format() String base64 = format.format(hash) assertEquals base64, hash.toBase64() @@ -41,7 +43,7 @@ class Base64FormatTest { @Test void testFormatWithNullArgument() { Base64Format format = new Base64Format() - assertNull format.format(null) + assertThrows NullPointerException.class, { format.format(null) } } } diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactoryTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactoryTest.groovy index 17ec82de5c..10ddc09906 100644 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactoryTest.groovy +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactoryTest.groovy @@ -18,8 +18,9 @@ */ package org.apache.shiro.crypto.hash.format -import org.apache.shiro.crypto.hash.Sha1Hash +import org.apache.shiro.crypto.hash.Sha512Hash import org.junit.Test + import static org.junit.Assert.* /** @@ -72,7 +73,7 @@ class DefaultHashFormatFactoryTest { @Test void testGetInstanceWithMcfFormattedString() { Shiro1CryptFormat format = new Shiro1CryptFormat() - def formatted = format.format(new Sha1Hash("test")) + def formatted = format.format(new Sha512Hash("test")) def factory = new DefaultHashFormatFactory() @@ -101,7 +102,7 @@ class DefaultHashFormatFactoryTest { void testMcfFormattedArgument() { def factory = new DefaultHashFormatFactory() - def hash = new Sha1Hash("test") + def hash = new Sha512Hash("test") def formatted = new Shiro1CryptFormat().format(hash) def instance = factory.getInstance(formatted) diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/HexFormatTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/HexFormatTest.groovy index de71cc1428..eaf0ac245c 100644 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/HexFormatTest.groovy +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/HexFormatTest.groovy @@ -19,9 +19,11 @@ package org.apache.shiro.crypto.hash.format import org.apache.shiro.crypto.hash.Hash -import org.apache.shiro.crypto.hash.Sha1Hash -import org.junit.Test -import static org.junit.Assert.* +import org.apache.shiro.crypto.hash.Sha512Hash +import org.junit.jupiter.api.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertThrows /** * Unit tests for the {@link HexFormat} implementation. @@ -32,7 +34,7 @@ class HexFormatTest { @Test void testFormat() { - Hash hash = new Sha1Hash("hello"); + Hash hash = new Sha512Hash("hello"); HexFormat format = new HexFormat() String hex = format.format(hash) assertEquals hex, hash.toHex() @@ -41,7 +43,7 @@ class HexFormatTest { @Test void testFormatWithNullArgument() { HexFormat format = new HexFormat() - assertNull format.format(null) + assertThrows NullPointerException, { format.format(null) } } } diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/ProvidedHashFormatTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/ProvidedHashFormatTest.groovy index 21229d852a..3ad8d6aa84 100644 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/ProvidedHashFormatTest.groovy +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/ProvidedHashFormatTest.groovy @@ -19,6 +19,7 @@ package org.apache.shiro.crypto.hash.format import org.junit.Test + import static org.junit.Assert.* /** @@ -31,7 +32,7 @@ class ProvidedHashFormatTest { @Test void testDefaults() { def set = ProvidedHashFormat.values() as Set - assertEquals 3, set.size() + assertEquals 4, set.size() assertTrue set.contains(ProvidedHashFormat.HEX) assertTrue set.contains(ProvidedHashFormat.BASE64) assertTrue set.contains(ProvidedHashFormat.SHIRO1) diff --git a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/Shiro1CryptFormatTest.groovy b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/Shiro1CryptFormatTest.groovy index 2b10c099f1..b4b38aab52 100644 --- a/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/Shiro1CryptFormatTest.groovy +++ b/crypto/hash/src/test/groovy/org/apache/shiro/crypto/hash/format/Shiro1CryptFormatTest.groovy @@ -21,6 +21,7 @@ package org.apache.shiro.crypto.hash.format import org.apache.shiro.crypto.SecureRandomNumberGenerator import org.apache.shiro.crypto.hash.SimpleHash import org.junit.Test + import static org.junit.Assert.* /** @@ -65,7 +66,7 @@ class Shiro1CryptFormatTest { def rng = new SecureRandomNumberGenerator() def source = rng.nextBytes() - def hash = new SimpleHash(alg, source, null, iterations) + def hash = new SimpleHash(alg, source, iterations) String formatted = format.format(hash); @@ -120,7 +121,7 @@ class Shiro1CryptFormatTest { def rng = new SecureRandomNumberGenerator() def source = rng.nextBytes() - def hash = new SimpleHash(alg, source, null, iterations) + def hash = new SimpleHash(alg, source, iterations) String formatted = Shiro1CryptFormat.MCF_PREFIX + alg + delim + @@ -133,7 +134,7 @@ class Shiro1CryptFormatTest { assertEquals hash, parsedHash assertEquals hash.algorithmName, parsedHash.algorithmName assertEquals hash.iterations, parsedHash.iterations - assertNull hash.salt + assertTrue hash.salt.isEmpty() assertTrue Arrays.equals(hash.bytes, parsedHash.bytes) } diff --git a/crypto/pom.xml b/crypto/pom.xml index b7f0e680c2..72dba2b027 100644 --- a/crypto/pom.xml +++ b/crypto/pom.xml @@ -36,6 +36,7 @@ core hash cipher + support diff --git a/crypto/support/hashes/argon2/pom.xml b/crypto/support/hashes/argon2/pom.xml new file mode 100644 index 0000000000..208a0542d0 --- /dev/null +++ b/crypto/support/hashes/argon2/pom.xml @@ -0,0 +1,79 @@ + + + + + 4.0.0 + + + org.apache.shiro.crypto + shiro-crypto-support + 2.0.0-SNAPSHOT + ../../pom.xml + + + shiro-hashes-argon2 + Apache Shiro :: Cryptography :: Support :: Hashes :: Argon2 + + bundle + + + + org.apache.shiro + shiro-crypto-hash + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.apache.shiro.hashes.argon2 + org.apache.hashes.argon2*;version=${project.version} + + org.apache.shiro*;version="${shiro.osgi.importRange}", + org.aopalliance*;version="[1.0.0, 2.0.0)", + com.google.inject*;version="1.3", + * + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + + + diff --git a/crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2Hash.java b/crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2Hash.java new file mode 100644 index 0000000000..fee355206c --- /dev/null +++ b/crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2Hash.java @@ -0,0 +1,349 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.shiro.crypto.support.hashes.argon2; + +import org.apache.shiro.crypto.hash.AbstractCryptHash; +import org.apache.shiro.lang.codec.Base64; +import org.apache.shiro.lang.util.ByteSource; +import org.apache.shiro.lang.util.SimpleByteSource; +import org.bouncycastle.crypto.generators.Argon2BytesGenerator; +import org.bouncycastle.crypto.params.Argon2Parameters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Base64.Encoder; +import java.util.HashSet; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.util.StringJoiner; +import java.util.regex.Pattern; + +import static java.util.Collections.unmodifiableSet; +import static java.util.Objects.requireNonNull; + +/** + * The Argon2 key derivation function (KDF) is a modern algorithm to shade and hash passwords. + * + *

    The default implementation ({@code argon2id}) is designed to use both memory and cpu to make + * brute force attacks unfeasible.

    + * + *

    The defaults are taken from + * argon2-cffi.readthedocs.io. + * The RFC suggests to use 1 GiB of memory for frontend and 4 GiB for backend authentication.

    + * + *

    Example crypt string is: {@code $argon2i$v=19$m=16384,t=100,p=2$M3ByeyZKLjFRREJqQi87WQ$5kRCtDjL6RoIWGq9bL27DkFNunucg1hW280PmP0XDtY}.

    + * + * @since 2.0 + */ +class Argon2Hash extends AbstractCryptHash { + private static final long serialVersionUID = 2647354947284558921L; + + private static final Logger LOG = LoggerFactory.getLogger(Argon2Hash.class); + + public static final String DEFAULT_ALGORITHM_NAME = "argon2id"; + + public static final int DEFAULT_ALGORITHM_VERSION = Argon2Parameters.ARGON2_VERSION_13; + + public static final int DEFAULT_ITERATIONS = 3; + + public static final int DEFAULT_MEMORY_KIB = 4096; + + private static final Set ALGORITHMS_ARGON2 = new HashSet<>(Arrays.asList("argon2id", "argon2i", "argon2d")); + + private static final Pattern DELIMITER_COMMA = Pattern.compile(","); + + + public static final int DEFAULT_PARALLELISM = 4; + + public static final int DEFAULT_OUTPUT_LENGTH = 32; + + + /** + * 128 bits of salt is the recommended salt length. + */ + private static final int SALT_LENGTH = 16; + + private final int argonVersion; + + private final int iterations; + + private final int memoryKiB; + + private final int parallelism; + + public Argon2Hash(String algorithmName, int argonVersion, byte[] hashedData, ByteSource salt, int iterations, int memoryAsKB, int parallelism) { + super(algorithmName, hashedData, salt); + this.argonVersion = argonVersion; + this.iterations = iterations; + this.memoryKiB = memoryAsKB; + this.parallelism = parallelism; + + checkValidIterations(); + } + + public static Set getAlgorithmsArgon2() { + return unmodifiableSet(ALGORITHMS_ARGON2); + } + + protected static ByteSource createSalt() { + return createSalt(new SecureRandom()); + } + + public static ByteSource createSalt(SecureRandom random) { + return new SimpleByteSource(random.generateSeed(SALT_LENGTH)); + } + + public static Argon2Hash fromString(String input) { + // expected: + // $argon2i$v=19$m=4096,t=3,p=4$M3ByeyZKLjFRREJqQi87WQ$5kRCtDjL6RoIWGq9bL27DkFNunucg1hW280PmP0XDtY + if (!input.startsWith("$")) { + throw new UnsupportedOperationException("Unsupported input: " + input); + } + + final String[] parts = AbstractCryptHash.DELIMITER.split(input.substring(1)); + final String algorithmName = parts[0].trim(); + + if (!ALGORITHMS_ARGON2.contains(algorithmName)) { + throw new UnsupportedOperationException("Unsupported algorithm: " + algorithmName + ". Expected one of: " + ALGORITHMS_ARGON2); + } + + final int version = parseVersion(parts[1]); + final String parameters = parts[2]; + final int memoryPowTwo = parseMemory(parameters); + final int iterations = parseIterations(parameters); + final int parallelism = parseParallelism(parameters); + final ByteSource salt = new SimpleByteSource(Base64.decode(parts[3])); + final byte[] hashedData = Base64.decode(parts[4]); + + return new Argon2Hash(algorithmName, version, hashedData, salt, iterations, memoryPowTwo, parallelism); + } + + private static int parseParallelism(String parameters) { + String parameter = DELIMITER_COMMA.splitAsStream(parameters) + .filter(parm -> parm.startsWith("p=")) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("Did not found memory parameter 'p='. Got: [" + parameters + "].")); + return Integer.parseInt(parameter.substring(2)); + } + + private static int parseIterations(String parameters) { + String parameter = DELIMITER_COMMA.splitAsStream(parameters) + .filter(parm -> parm.startsWith("t=")) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("Did not found memory parameter 't='. Got: [" + parameters + "].")); + + return Integer.parseInt(parameter.substring(2)); + } + + private static int parseMemory(String parameters) { + String parameter = DELIMITER_COMMA.splitAsStream(parameters) + .filter(parm -> parm.startsWith("m=")) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("Did not found memory parameter 'm='. Got: [" + parameters + "].")); + + return Integer.parseInt(parameter.substring(2)); + } + + private static int parseVersion(final String part) { + if (!part.startsWith("v=")) { + throw new IllegalArgumentException("Did not find version parameter 'v='. Got: [" + part + "]."); + } + + return Integer.parseInt(part.substring(2)); + } + + public static Argon2Hash generate(final char[] source) { + return generate(new SimpleByteSource(source), createSalt(), DEFAULT_ITERATIONS); + } + + public static Argon2Hash generate(final ByteSource source, final ByteSource salt, final int iterations) { + return generate(DEFAULT_ALGORITHM_NAME, source, requireNonNull(salt, "salt"), iterations); + } + + public static Argon2Hash generate(String algorithmName, ByteSource source, ByteSource salt, int iterations) { + return generate(algorithmName, DEFAULT_ALGORITHM_VERSION, source, salt, iterations, DEFAULT_MEMORY_KIB, DEFAULT_PARALLELISM, DEFAULT_OUTPUT_LENGTH); + } + + public static Argon2Hash generate( + String algorithmName, + int argonVersion, + ByteSource source, + ByteSource salt, + int iterations, + int memoryAsKB, + int parallelism, + int outputLength + ) { + final int type; + switch (requireNonNull(algorithmName, "algorithmName")) { + case "argon2i": + type = Argon2Parameters.ARGON2_i; + break; + case "argon2d": + type = Argon2Parameters.ARGON2_d; + break; + case "argon2": + // fall through + case "argon2id": + type = Argon2Parameters.ARGON2_id; + break; + default: + throw new IllegalArgumentException("Unknown argon2 algorithm: " + algorithmName); + } + + final Argon2Parameters parameters = new Argon2Parameters.Builder(type) + .withVersion(argonVersion) + .withIterations(iterations) + .withParallelism(parallelism) + .withSalt(requireNonNull(salt, "salt").getBytes()) + .withMemoryAsKB(memoryAsKB) + .build(); + + final Argon2BytesGenerator gen = new Argon2BytesGenerator(); + gen.init(parameters); + + final byte[] hash = new byte[outputLength]; + gen.generateBytes(source.getBytes(), hash); + + return new Argon2Hash(algorithmName, argonVersion, hash, new SimpleByteSource(salt), iterations, memoryAsKB, parallelism); + } + + @Override + protected void checkValidAlgorithm() { + if (!ALGORITHMS_ARGON2.contains(getAlgorithmName())) { + final String message = String.format( + Locale.ENGLISH, + "Given algorithm name [%s] not valid for argon2. " + + "Valid algorithms: [%s].", + getAlgorithmName(), + ALGORITHMS_ARGON2 + ); + throw new IllegalArgumentException(message); + } + } + + protected void checkValidIterations() { + int iterations = this.getIterations(); + if (iterations < 1) { + final String message = String.format( + Locale.ENGLISH, + "Expected argon2 iterations >= 1, but was [%d].", + iterations + ); + throw new IllegalArgumentException(message); + } + } + + @Override + public int getIterations() { + return this.iterations; + } + + @Override + public boolean matchesPassword(ByteSource plaintextBytes) { + try { + Argon2Hash compare = generate(this.getAlgorithmName(), this.argonVersion, plaintextBytes, this.getSalt(), this.getIterations(), this.memoryKiB, this.parallelism, this.getBytes().length); + + return this.equals(compare); + } catch (IllegalArgumentException illegalArgumentException) { + // cannot recreate hash. Do not log password. + LOG.warn("Cannot recreate a hash using the same parameters.", illegalArgumentException); + return false; + } + } + + @Override + public int getSaltLength() { + return SALT_LENGTH; + } + + @Override + public String formatToCryptString() { + // expected: + // $argon2i$v=19$m=4096,t=3,p=4$M3ByeyZKLjFRREJqQi87WQ$5kRCtDjL6RoIWGq9bL27DkFNunucg1hW280PmP0XDtY + Encoder encoder = java.util.Base64.getEncoder().withoutPadding(); + String saltBase64 = encoder.encodeToString(this.getSalt().getBytes()); + String dataBase64 = encoder.encodeToString(this.getBytes()); + + return new StringJoiner("$", "$", "") + .add(this.getAlgorithmName()) + .add("v=" + this.argonVersion) + .add(formatParameters()) + .add(saltBase64) + .add(dataBase64) + .toString(); + } + + private CharSequence formatParameters() { + return String.format( + Locale.ENGLISH, + "t=%d,m=%d,p=%d", + getIterations(), + getMemoryKiB(), + getParallelism() + ); + } + + public int getMemoryKiB() { + return memoryKiB; + } + + public int getParallelism() { + return parallelism; + } + + public int getArgonVersion() { + return argonVersion; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + if (!super.equals(other)) { + return false; + } + Argon2Hash that = (Argon2Hash) other; + return argonVersion == that.argonVersion && iterations == that.iterations && memoryKiB == that.memoryKiB && parallelism == that.parallelism; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), argonVersion, iterations, memoryKiB, parallelism); + } + + @Override + public String toString() { + return new StringJoiner(", ", Argon2Hash.class.getSimpleName() + "[", "]") + .add("super=" + super.toString()) + .add("version=" + argonVersion) + .add("iterations=" + iterations) + .add("memoryKiB=" + memoryKiB) + .add("parallelism=" + parallelism) + .toString(); + } +} diff --git a/crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashProvider.java b/crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashProvider.java new file mode 100644 index 0000000000..fdacbc3729 --- /dev/null +++ b/crypto/support/hashes/argon2/src/main/java/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashProvider.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.shiro.crypto.support.hashes.argon2; + +import org.apache.shiro.crypto.hash.HashRequest; +import org.apache.shiro.crypto.hash.HashSpi; +import org.apache.shiro.lang.util.ByteSource; +import org.apache.shiro.lang.util.SimpleByteSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.SecureRandom; +import java.util.Base64; +import java.util.Locale; +import java.util.Optional; +import java.util.Random; +import java.util.Set; + +/** + * A HashProvider for the Argon2 hash algorithm. + * + *

    This class is intended to be used by the {@code HashProvider} class from Shiro. However, + * this class can also be used to created instances of the Argon2 hash manually.

    + * + *

    Furthermore, there is a nested {@link Parameters} class which provides names for the + * keys used in the parameters map of the {@link HashRequest} class.

    + * + * @since 2.0 + */ +public class Argon2HashProvider implements HashSpi { + + private static final Logger LOG = LoggerFactory.getLogger(Argon2HashProvider.class); + + @Override + public Set getImplementedAlgorithms() { + return Argon2Hash.getAlgorithmsArgon2(); + } + + @Override + public Argon2Hash fromString(String format) { + return Argon2Hash.fromString(format); + } + + @Override + public HashFactory newHashFactory(Random random) { + return new Argon2HashFactory(random); + } + + static class Argon2HashFactory implements HashSpi.HashFactory { + + private final SecureRandom random; + + public Argon2HashFactory(Random random) { + if (!(random instanceof SecureRandom)) { + throw new IllegalArgumentException("Only SecureRandom instances are supported at the moment!"); + } + + this.random = (SecureRandom) random; + } + + @Override + public Argon2Hash generate(HashRequest hashRequest) { + final String algorithmName = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_ALGORITHM_NAME)) + .map(algo -> (String) algo) + .orElse(Parameters.DEFAULT_ALGORITHM_NAME); + + final int version = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_ALGORITHM_VERSION)) + .flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_ALGORITHM_VERSION)) + .orElse(Parameters.DEFAULT_ALGORITHM_VERSION); + + final ByteSource salt = parseSalt(hashRequest); + + final int iterations = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_ITERATIONS)) + .flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_ITERATIONS)) + .orElse(Parameters.DEFAULT_ITERATIONS); + + final int memoryKib = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_MEMORY_KIB)) + .flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_MEMORY_KIB)) + .orElse(Parameters.DEFAULT_MEMORY_KIB); + + final int parallelism = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_PARALLELISM)) + .flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_PARALLELISM)) + .orElse(Parameters.DEFAULT_PARALLELISM); + + final int outputLength = Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_OUTPUT_LENGTH)) + .flatMap(algoV -> intOrEmpty(algoV, Parameters.PARAMETER_OUTPUT_LENGTH)) + .orElse(Parameters.DEFAULT_OUTPUT_LENGTH); + + return Argon2Hash.generate( + algorithmName, + version, + hashRequest.getSource(), + salt, + iterations, + memoryKib, + parallelism, + outputLength + ); + } + + private ByteSource parseSalt(HashRequest hashRequest) { + return Optional.ofNullable(hashRequest.getParameters().get(Parameters.PARAMETER_SALT)) + .map(saltParm -> Base64.getDecoder().decode((String) saltParm)) + .map(SimpleByteSource::new) + .flatMap(this::lengthValidOrEmpty) + .orElseGet(() -> Argon2Hash.createSalt(random)); + } + + private Optional lengthValidOrEmpty(ByteSource bytes) { + if (bytes.getBytes().length != 16) { + return Optional.empty(); + } + + return Optional.of(bytes); + } + + private Optional intOrEmpty(Object maybeInt, String parameterName) { + try { + return Optional.of(Integer.parseInt((String) maybeInt, 10)); + } catch (NumberFormatException numberFormatException) { + String message = String.format( + Locale.ENGLISH, + "Expected Integer for parameter %s, but %s is not parsable.", + parameterName, maybeInt + ); + LOG.warn(message, numberFormatException); + return Optional.empty(); + } + } + } + + /** + * Parameters for the {@link Argon2Hash} class. + * + *

    This class contains public constants only. The constants starting with {@code PARAMETER_} are + * the parameter names recognized by the + * {@link org.apache.shiro.crypto.hash.HashSpi.HashFactory#generate(HashRequest)} method.

    + * + *

    The constants starting with {@code DEFAULT_} are their respective default values.

    + */ + public static final class Parameters { + + public static final String DEFAULT_ALGORITHM_NAME = Argon2Hash.DEFAULT_ALGORITHM_NAME; + public static final int DEFAULT_ALGORITHM_VERSION = Argon2Hash.DEFAULT_ALGORITHM_VERSION; + public static final int DEFAULT_ITERATIONS = Argon2Hash.DEFAULT_ITERATIONS; + public static final int DEFAULT_MEMORY_KIB = Argon2Hash.DEFAULT_MEMORY_KIB; + public static final int DEFAULT_PARALLELISM = Argon2Hash.DEFAULT_PARALLELISM; + public static final int DEFAULT_OUTPUT_LENGTH = Argon2Hash.DEFAULT_OUTPUT_LENGTH; + + /** + * Parameter for modifying the internal algorithm used by Argon2. + * + *

    Valid values are {@code argon2i} (optimized to resist side-channel attacks), + * {@code argon2d} (maximizes resistance to GPU cracking attacks) + * and {@code argon2id} (a hybrid version).

    + * + *

    The default value is {@value DEFAULT_ALGORITHM_NAME} when this parameter is not specified.

    + */ + public static final String PARAMETER_ALGORITHM_NAME = "Argon2.algorithmName"; + public static final String PARAMETER_ALGORITHM_VERSION = "Argon2.version"; + + /** + * The salt to use. + * + *

    The value for this parameter accepts a Base64-encoded 16byte (128bit) salt.

    + * + *

    As for any KDF, do not use a static salt value for multiple passwords.

    + * + *

    The default value is a new random 128bit-salt, if this parameter is not specified.

    + */ + public static final String PARAMETER_SALT = "Argon2.salt"; + + public static final String PARAMETER_ITERATIONS = "Argon2.iterations"; + public static final String PARAMETER_MEMORY_KIB = "Argon2.memoryKib"; + public static final String PARAMETER_PARALLELISM = "Argon2.parallelism"; + + /** + * The output length of the resulting data section. + * + *

    Argon2 allows to modify the length of the generated output.

    + * + *

    The default value is {@value DEFAULT_OUTPUT_LENGTH} when this parameter is not specified.

    + */ + public static final String PARAMETER_OUTPUT_LENGTH = "Argon2.outputLength"; + + private Parameters() { + // utility class + } + } +} diff --git a/crypto/support/hashes/argon2/src/main/resources/META-INF/NOTICE b/crypto/support/hashes/argon2/src/main/resources/META-INF/NOTICE new file mode 100644 index 0000000000..4b3b138f99 --- /dev/null +++ b/crypto/support/hashes/argon2/src/main/resources/META-INF/NOTICE @@ -0,0 +1,18 @@ +Apache Shiro +Copyright 2008-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +The implementation for org.apache.shiro.util.SoftHashMap is based +on initial ideas from Dr. Heinz Kabutz's publicly posted version +available at http://www.javaspecialists.eu/archive/Issue015.html, +with continued modifications. + +The implementation of Radix64 aka BcryptBase64 (OpenBSD BCrypt’s Base64) was copied +from https://github.com/patrickfav/bcrypt. + +Certain parts (StringUtils, IpAddressMatcher, etc.) of the source +code for this product was copied for simplicity and to reduce +dependencies from the source code developed by the Spring Framework +Project (http://www.springframework.org). diff --git a/crypto/support/hashes/argon2/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi b/crypto/support/hashes/argon2/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi new file mode 100644 index 0000000000..80a9e65fec --- /dev/null +++ b/crypto/support/hashes/argon2/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +org.apache.shiro.crypto.support.hashes.argon2.Argon2HashProvider diff --git a/crypto/support/hashes/argon2/src/test/groovy/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashTest.groovy b/crypto/support/hashes/argon2/src/test/groovy/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashTest.groovy new file mode 100644 index 0000000000..638fa8dd4a --- /dev/null +++ b/crypto/support/hashes/argon2/src/test/groovy/org/apache/shiro/crypto/support/hashes/argon2/Argon2HashTest.groovy @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.shiro.crypto.support.hashes.argon2 + +import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat +import org.apache.shiro.crypto.hash.format.Shiro2CryptFormat +import org.apache.shiro.lang.util.SimpleByteSource +import org.bouncycastle.crypto.params.Argon2Parameters +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.function.Executable + +import static org.junit.jupiter.api.Assertions.* + +class Argon2HashTest { + + private static final TEST_PASSWORD = "secret#shiro,password;Jo8opech"; + private static final TEST_PASSWORD_BS = new SimpleByteSource(TEST_PASSWORD) + + @Test + void testArgon2Hash() { + // given + def shiro2Format = '$shiro2$argon2id$v=19$m=4096,t=3,p=4$MTIzNDU2Nzg5MDEyMzQ1Ng$bjcHqfb0LPHyS13eVaNcBga9LF12I3k34H5ULt2gyoI' + def expectedPassword = new SimpleByteSource('secret#shiro,password;Jo8opech') + + // when + def hash = new Shiro2CryptFormat().parse(shiro2Format) as Argon2Hash; + System.out.println("Hash: " + hash) + def matchesPassword = hash.matchesPassword expectedPassword; + + // then + assertEquals Argon2Parameters.ARGON2_VERSION_13, hash.argonVersion + assertEquals 3, hash.iterations + assertEquals 4096, hash.memoryKiB + assertEquals 4, hash.parallelism + assertTrue matchesPassword + } + + @Test + void testArgon2HashShiro1Format() { + // given + def shiro1Format = '$shiro1$argon2id$v=19$t=2,m=131072,p=4$7858qTJTreh61AzFV2XMOw==$lLzl2VNNbyFcuJo0Hp7JQpguKCDoQwxo91AWobcHzeo=' + + // when + def thrownException = assertThrows( + UnsupportedOperationException, + { new Shiro1CryptFormat().parse shiro1Format } as Executable + ) + + // then + assertTrue thrownException.getMessage().contains("shiro1") + } + + @Test + void testFromStringMatchesPw() { + // when + def argon2String = '$argon2id$v=19$m=4096,t=3,p=4$MTIzNDU2Nzg5MDEyMzQ1Ng$bjcHqfb0LPHyS13eVaNcBga9LF12I3k34H5ULt2gyoI' + // for testing recreated salt and data parts, as the parameter order could change. + def saltDataPart = argon2String.substring(30) + + // when + def argon2Hash = Argon2Hash.fromString argon2String + def recreatedSaltDataPart = argon2Hash.formatToCryptString().substring(30) + + // then + assertTrue argon2Hash.matchesPassword(TEST_PASSWORD_BS) + // we can only test the salt + data parts, as + // the parameter order could change. + assertEquals saltDataPart, recreatedSaltDataPart + } + +} diff --git a/crypto/support/hashes/bcrypt/pom.xml b/crypto/support/hashes/bcrypt/pom.xml new file mode 100644 index 0000000000..24924d7833 --- /dev/null +++ b/crypto/support/hashes/bcrypt/pom.xml @@ -0,0 +1,79 @@ + + + + + 4.0.0 + + + org.apache.shiro.crypto + shiro-crypto-support + 2.0.0-SNAPSHOT + ../../pom.xml + + + shiro-hashes-bcrypt + Apache Shiro :: Cryptography :: Support :: Hashes :: BCrypt + + bundle + + + + org.apache.shiro + shiro-crypto-hash + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.apache.shiro.hashes.bcrypt + org.apache.hashes.bcrypt*;version=${project.version} + + org.apache.shiro*;version="${shiro.osgi.importRange}", + org.aopalliance*;version="[1.0.0, 2.0.0)", + com.google.inject*;version="1.3", + * + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + + + diff --git a/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHash.java b/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHash.java new file mode 100644 index 0000000000..f73b40a560 --- /dev/null +++ b/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHash.java @@ -0,0 +1,200 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.shiro.crypto.support.hashes.bcrypt; + +import org.apache.shiro.crypto.hash.AbstractCryptHash; +import org.apache.shiro.lang.util.ByteSource; +import org.apache.shiro.lang.util.SimpleByteSource; +import org.bouncycastle.crypto.generators.OpenBSDBCrypt; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import java.util.StringJoiner; + +import static java.util.Collections.unmodifiableSet; + +/** + * @since 2.0 + */ +class BCryptHash extends AbstractCryptHash { + + private static final long serialVersionUID = 6957869292324606101L; + + private static final Logger LOG = LoggerFactory.getLogger(AbstractCryptHash.class); + + public static final String DEFAULT_ALGORITHM_NAME = "2y"; + + public static final int DEFAULT_COST = 10; + + public static final int SALT_LENGTH = 16; + + private static final Set ALGORITHMS_BCRYPT = new HashSet<>(Arrays.asList("2", "2a", "2b", "2y")); + + private final int cost; + + private final int iterations; + + public BCryptHash(final String version, final byte[] hashedData, final ByteSource salt, final int cost) { + super(version, hashedData, salt); + this.cost = cost; + this.iterations = (int) Math.pow(2, cost); + checkValidCost(); + } + + @Override + protected final void checkValidAlgorithm() { + if (!ALGORITHMS_BCRYPT.contains(getAlgorithmName())) { + final String message = String.format( + Locale.ENGLISH, + "Given algorithm name [%s] not valid for bcrypt. " + + "Valid algorithms: [%s].", + getAlgorithmName(), + ALGORITHMS_BCRYPT + ); + throw new IllegalArgumentException(message); + } + } + + protected final void checkValidCost() { + checkValidCost(this.cost); + } + + public static int checkValidCost(final int cost) { + if (cost < 4 || cost > 31) { + final String message = String.format( + Locale.ENGLISH, + "Expected bcrypt cost >= 4 and <=30, but was [%d].", + cost + ); + throw new IllegalArgumentException(message); + } + + return cost; + } + + public int getCost() { + return this.cost; + } + + public static Set getAlgorithmsBcrypt() { + return unmodifiableSet(ALGORITHMS_BCRYPT); + } + + public static BCryptHash fromString(String input) { + // the input string should look like this: + // $2y$cost$salt{22}hash + if (!input.startsWith("$")) { + throw new IllegalArgumentException("Unsupported input: " + input); + } + + final String[] parts = AbstractCryptHash.DELIMITER.split(input.substring(1)); + + if (parts.length != 3) { + throw new IllegalArgumentException("Expected string containing three '$' but got: '" + Arrays.toString(parts) + "'."); + } + final String algorithmName = parts[0].trim(); + final int cost = Integer.parseInt(parts[1].trim(), 10); + + final String dataSection = parts[2]; + final OpenBSDBase64.Default bcryptBase64 = new OpenBSDBase64.Default(); + + final String saltBase64 = dataSection.substring(0, 22); + final String bytesBase64 = dataSection.substring(22); + final byte[] salt = bcryptBase64.decode(saltBase64.getBytes(StandardCharsets.ISO_8859_1)); + final byte[] hashedData = bcryptBase64.decode(bytesBase64.getBytes(StandardCharsets.ISO_8859_1)); + + return new BCryptHash(algorithmName, hashedData, new SimpleByteSource(salt), cost); + } + + public static BCryptHash generate(final ByteSource source) { + return generate(source, createSalt(), DEFAULT_COST); + } + + + public static BCryptHash generate(final ByteSource source, final ByteSource initialSalt, final int cost) { + return generate(DEFAULT_ALGORITHM_NAME, source, initialSalt, cost); + } + + public static BCryptHash generate(String algorithmName, ByteSource source, ByteSource salt, int cost) { + checkValidCost(cost); + final String cryptString = OpenBSDBCrypt.generate(algorithmName, source.getBytes(), salt.getBytes(), cost); + + return fromString(cryptString); + } + + protected static ByteSource createSalt() { + return createSalt(new SecureRandom()); + } + + protected static ByteSource createSalt(SecureRandom random) { + return new SimpleByteSource(random.generateSeed(SALT_LENGTH)); + } + + @Override + public int getSaltLength() { + return SALT_LENGTH; + } + + @Override + public String formatToCryptString() { + OpenBSDBase64.Default bsdBase64 = new OpenBSDBase64.Default(); + String saltBase64 = new String(bsdBase64.encode(this.getSalt().getBytes()), StandardCharsets.ISO_8859_1); + String dataBase64 = new String(bsdBase64.encode(this.getBytes()), StandardCharsets.ISO_8859_1); + + return new StringJoiner("$", "$", "") + .add(this.getAlgorithmName()) + .add("" + this.cost) + .add(saltBase64 + dataBase64) + .toString(); + } + + @Override + public int getIterations() { + return this.iterations; + } + + @Override + public boolean matchesPassword(ByteSource plaintextBytes) { + try { + final String cryptString = OpenBSDBCrypt.generate(this.getAlgorithmName(), plaintextBytes.getBytes(), this.getSalt().getBytes(), this.getCost()); + BCryptHash other = fromString(cryptString); + + return this.equals(other); + } catch (IllegalArgumentException illegalArgumentException) { + // cannot recreate hash. Do not log password. + LOG.warn("Cannot recreate a hash using the same parameters.", illegalArgumentException); + return false; + } + } + + @Override + public String toString() { + return new StringJoiner(", ", BCryptHash.class.getSimpleName() + "[", "]") + .add("super=" + super.toString()) + .add("cost=" + this.cost) + .toString(); + } +} diff --git a/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptProvider.java b/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptProvider.java new file mode 100644 index 0000000000..74961564b3 --- /dev/null +++ b/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptProvider.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.shiro.crypto.support.hashes.bcrypt; + +import org.apache.shiro.crypto.hash.HashRequest; +import org.apache.shiro.crypto.hash.HashSpi; +import org.apache.shiro.lang.util.ByteSource; +import org.apache.shiro.lang.util.SimpleByteSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.SecureRandom; +import java.util.Base64; +import java.util.Locale; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Random; +import java.util.Set; + +/** + * @since 2.0 + */ +public class BCryptProvider implements HashSpi { + + private static final Logger LOG = LoggerFactory.getLogger(BCryptProvider.class); + + @Override + public Set getImplementedAlgorithms() { + return BCryptHash.getAlgorithmsBcrypt(); + } + + @Override + public BCryptHash fromString(String format) { + return BCryptHash.fromString(format); + } + + @Override + public HashFactory newHashFactory(Random random) { + return new BCryptHashFactory(random); + } + + static class BCryptHashFactory implements HashSpi.HashFactory { + + private final SecureRandom random; + + public BCryptHashFactory(Random random) { + if (!(random instanceof SecureRandom)) { + throw new IllegalArgumentException("Only SecureRandom instances are supported at the moment!"); + } + + this.random = (SecureRandom) random; + } + + @Override + public BCryptHash generate(HashRequest hashRequest) { + final String algorithmName = hashRequest.getAlgorithmName().orElse(Parameters.DEFAULT_ALGORITHM_NAME); + + final ByteSource salt = getSalt(hashRequest); + + final int cost = getCost(hashRequest); + + return BCryptHash.generate( + algorithmName, + hashRequest.getSource(), + salt, + cost + ); + } + + private int getCost(HashRequest hashRequest) { + final Map parameters = hashRequest.getParameters(); + final Optional optCostStr = Optional.ofNullable(parameters.get(Parameters.PARAMETER_COST)) + .map(obj -> (String) obj); + + if (!optCostStr.isPresent()) { + return BCryptHash.DEFAULT_COST; + } + + String costStr = optCostStr.orElseThrow(NoSuchElementException::new); + try { + int cost = Integer.parseInt(costStr, 10); + BCryptHash.checkValidCost(cost); + return cost; + } catch (IllegalArgumentException costEx) { + String message = String.format( + Locale.ENGLISH, + "Expected Integer for parameter %s, but %s is not parsable or valid.", + Parameters.PARAMETER_COST, costStr + ); + LOG.warn(message, costEx); + + return BCryptHash.DEFAULT_COST; + } + } + + private ByteSource getSalt(HashRequest hashRequest) { + final Map parameters = hashRequest.getParameters(); + final Optional optSaltBase64 = Optional.ofNullable(parameters.get(Parameters.PARAMETER_SALT)) + .map(obj -> (String) obj); + + if (!optSaltBase64.isPresent()) { + return BCryptHash.createSalt(random); + } + + final String saltBase64 = optSaltBase64.orElseThrow(NoSuchElementException::new); + final byte[] saltBytes = Base64.getDecoder().decode(saltBase64); + + if (saltBytes.length != BCryptHash.SALT_LENGTH) { + return BCryptHash.createSalt(random); + } + + return new SimpleByteSource(saltBytes); + } + } + + public static final class Parameters { + public static final String DEFAULT_ALGORITHM_NAME = BCryptHash.DEFAULT_ALGORITHM_NAME; + + public static final String PARAMETER_SALT = "BCrypt.salt"; + public static final String PARAMETER_COST = "BCrypt.cost"; + + private Parameters() { + // utility class + } + } +} diff --git a/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/OpenBSDBase64.java b/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/OpenBSDBase64.java new file mode 100644 index 0000000000..ad05fe853b --- /dev/null +++ b/crypto/support/hashes/bcrypt/src/main/java/org/apache/shiro/crypto/support/hashes/bcrypt/OpenBSDBase64.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.shiro.crypto.support.hashes.bcrypt; + + +/** + * Encoder for the custom Base64 variant of BCrypt (called Radix64 here). It has the same rules as Base64 but uses a + * different mapping table than the various RFCs + *

    + * According to Wikipedia: + * + *

    + * Unix stores password hashes computed with crypt in the /etc/passwd file using radix-64 encoding called B64. It uses a + * mostly-alphanumeric set of characters, plus . and /. Its 64-character set is "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz". + * Padding is not used. + *
    + * + * @since 2.0 + */ +interface OpenBSDBase64 { + + + /** + * Encode given raw byte array to a Radix64 style, UTF-8 encoded byte array. + * + * @param rawBytes to encode + * @return UTF-8 encoded string representing radix64 encoded data + */ + byte[] encode(byte[] rawBytes); + + /** + * From a UTF-8 encoded string representing radix64 encoded data as byte array, decodes the raw bytes from it. + * + * @param utf8EncodedRadix64String from a string get it with "m0CrhHm10qJ3lXRY.5zDGO".getBytes(StandardCharsets.UTF8) + * @return the raw bytes encoded by this utf-8 radix4 string + */ + byte[] decode(byte[] utf8EncodedRadix64String); + + /** + * A mod of Square's Okio Base64 encoder + *

    + * Original author: Alexander Y. Kleymenov + * + * @see Okio + */ + class Default implements OpenBSDBase64 { + private static final byte[] DECODE_TABLE = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 54, 55, 56, 57, + 58, 59, 60, 61, 62, 63, -1, -1, -1, -2, -1, -1, -1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, -1, -1, -1, -1, -1, -1, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, + 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53}; + + private static final byte[] MAP = new byte[]{ + '.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', + 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', + '6', '7', '8', '9' + }; + + @Override + public byte[] encode(final byte[] in) { + return encode(in, MAP); + } + + @Override + public byte[] decode(final byte[] in) { + // Ignore trailing '=' padding and whitespace from the input. + int limit = in.length; + for (; limit > 0; limit--) { + final byte c = in[limit - 1]; + if (c != '=' && c != '\n' && c != '\r' && c != ' ' && c != '\t') { + break; + } + } + + // If the input includes whitespace, this output array will be longer than necessary. + final byte[] out = new byte[(int) (limit * 6L / 8L)]; + int outCount = 0; + int inCount = 0; + + int word = 0; + for (int pos = 0; pos < limit; pos++) { + final byte c = in[pos]; + + final int bits; + if (c == '.' || c == '/' || (c >= 'A' && c <= 'z') || (c >= '0' && c <= '9')) { + bits = DECODE_TABLE[c]; + } else if (c == '\n' || c == '\r' || c == ' ' || c == '\t') { + continue; + } else { + throw new IllegalArgumentException("invalid character to decode: " + c); + } + + // Append this char's 6 bits to the word. + word = (word << 6) | (byte) bits; + + // For every 4 chars of input, we accumulate 24 bits of output. Emit 3 bytes. + inCount++; + if (inCount % 4 == 0) { + out[outCount++] = (byte) (word >> 16); + out[outCount++] = (byte) (word >> 8); + out[outCount++] = (byte) word; + } + } + + final int lastWordChars = inCount % 4; + if (lastWordChars == 1) { + // We read 1 char followed by "===". But 6 bits is a truncated byte! Fail. + return new byte[0]; + } else if (lastWordChars == 2) { + // We read 2 chars followed by "==". Emit 1 byte with 8 of those 12 bits. + word = word << 12; + out[outCount++] = (byte) (word >> 16); + } else if (lastWordChars == 3) { + // We read 3 chars, followed by "=". Emit 2 bytes for 16 of those 18 bits. + word = word << 6; + out[outCount++] = (byte) (word >> 16); + out[outCount++] = (byte) (word >> 8); + } + + // If we sized our out array perfectly, we're done. + if (outCount == out.length) { + return out; + } + + // Copy the decoded bytes to a new, right-sized array. + final byte[] prefix = new byte[outCount]; + System.arraycopy(out, 0, prefix, 0, outCount); + return prefix; + } + + private static byte[] encode(final byte[] in, final byte[] map) { + final int length = 4 * (in.length / 3) + (in.length % 3 == 0 ? 0 : in.length % 3 + 1); + final byte[] out = new byte[length]; + int index = 0; + final int end = in.length - in.length % 3; + for (int i = 0; i < end; i += 3) { + out[index++] = map[(in[i] & 0xff) >> 2]; + out[index++] = map[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)]; + out[index++] = map[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)]; + out[index++] = map[(in[i + 2] & 0x3f)]; + } + switch (in.length % 3) { + case 1: + out[index++] = map[(in[end] & 0xff) >> 2]; + out[index] = map[(in[end] & 0x03) << 4]; + break; + case 2: + out[index++] = map[(in[end] & 0xff) >> 2]; + out[index++] = map[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)]; + out[index] = map[((in[end + 1] & 0x0f) << 2)]; + break; + } + return out; + } + } +} diff --git a/crypto/support/hashes/bcrypt/src/main/resources/META-INF/NOTICE b/crypto/support/hashes/bcrypt/src/main/resources/META-INF/NOTICE new file mode 100644 index 0000000000..4b3b138f99 --- /dev/null +++ b/crypto/support/hashes/bcrypt/src/main/resources/META-INF/NOTICE @@ -0,0 +1,18 @@ +Apache Shiro +Copyright 2008-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +The implementation for org.apache.shiro.util.SoftHashMap is based +on initial ideas from Dr. Heinz Kabutz's publicly posted version +available at http://www.javaspecialists.eu/archive/Issue015.html, +with continued modifications. + +The implementation of Radix64 aka BcryptBase64 (OpenBSD BCrypt’s Base64) was copied +from https://github.com/patrickfav/bcrypt. + +Certain parts (StringUtils, IpAddressMatcher, etc.) of the source +code for this product was copied for simplicity and to reduce +dependencies from the source code developed by the Spring Framework +Project (http://www.springframework.org). diff --git a/crypto/support/hashes/bcrypt/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi b/crypto/support/hashes/bcrypt/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi new file mode 100644 index 0000000000..95d1df35c6 --- /dev/null +++ b/crypto/support/hashes/bcrypt/src/main/resources/META-INF/services/org.apache.shiro.crypto.hash.HashSpi @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +org.apache.shiro.crypto.support.hashes.bcrypt.BCryptProvider diff --git a/crypto/support/hashes/bcrypt/src/test/groovy/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHashTest.groovy b/crypto/support/hashes/bcrypt/src/test/groovy/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHashTest.groovy new file mode 100644 index 0000000000..f95e1a22f8 --- /dev/null +++ b/crypto/support/hashes/bcrypt/src/test/groovy/org/apache/shiro/crypto/support/hashes/bcrypt/BCryptHashTest.groovy @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.shiro.crypto.support.hashes.bcrypt + +import org.apache.shiro.lang.util.SimpleByteSource +import org.junit.jupiter.api.Test + +import java.nio.charset.StandardCharsets +import java.security.SecureRandom + +import static java.lang.Math.pow +import static org.junit.jupiter.api.Assertions.assertEquals +import static org.junit.jupiter.api.Assertions.assertTrue + +/** + * @since 2.0 + */ +class BCryptHashTest { + + private static final String TEST_PASSWORD = "secret#shiro,password;Jo8opech"; + + @Test + void testCreateHashGenerateSaltIterations() { + // given + final def testPasswordChars = new SimpleByteSource(TEST_PASSWORD) + + // when + final def bCryptHash = BCryptHash.generate testPasswordChars; + + // then + assertEquals BCryptHash.DEFAULT_COST, bCryptHash.cost; + } + + @Test + void testCreateHashGivenSalt() { + // given + final def testPasswordChars = new SimpleByteSource(TEST_PASSWORD); + final def salt = new SimpleByteSource(new SecureRandom().generateSeed(16)) + final def cost = 6 + + // when + final def bCryptHash = BCryptHash.generate(testPasswordChars, salt, cost); + + // then + assertEquals cost, bCryptHash.cost; + assertEquals pow(2, cost) as int, bCryptHash.iterations; + assertEquals salt, bCryptHash.salt; + } + + @Test + void toBase64EqualsInput() { + // given + def salt = '7rOjsAf2U/AKKqpMpCIn6e' + def saltBytes = new SimpleByteSource(new OpenBSDBase64.Default().decode(salt.getBytes(StandardCharsets.ISO_8859_1))) + def testPwBytes = new SimpleByteSource(TEST_PASSWORD) + def expectedHashString = '$2y$10$' + salt + 'tuOXyQ86tp2Tn9xv6FyXl2T0QYc3.G.' + + + // when + def bCryptHash = BCryptHash.generate("2y", testPwBytes, saltBytes, 10) + + // then + assertEquals expectedHashString, bCryptHash.formatToCryptString() + } + + @Test + void testMatchesPassword() { + // given + def expectedHashString = '$2y$10$7rOjsAf2U/AKKqpMpCIn6etuOXyQ86tp2Tn9xv6FyXl2T0QYc3.G.' + def bCryptHash = BCryptHash.fromString(expectedHashString) + def testPwBytes = new SimpleByteSource(TEST_PASSWORD) + + // when + def matchesPassword = bCryptHash.matchesPassword testPwBytes + + + // then + assertTrue matchesPassword + } + +} diff --git a/crypto/support/pom.xml b/crypto/support/pom.xml new file mode 100644 index 0000000000..582fe24b6d --- /dev/null +++ b/crypto/support/pom.xml @@ -0,0 +1,44 @@ + + + + + 4.0.0 + + + org.apache.shiro + shiro-crypto + 2.0.0-SNAPSHOT + ../pom.xml + + + org.apache.shiro.crypto + shiro-crypto-support + Apache Shiro :: Cryptography :: Support + pom + + + hashes/argon2 + hashes/bcrypt + + + + diff --git a/lang/src/main/java/org/apache/shiro/lang/codec/CodecSupport.java b/lang/src/main/java/org/apache/shiro/lang/codec/CodecSupport.java index e503f7e2e4..d7fd0c8837 100644 --- a/lang/src/main/java/org/apache/shiro/lang/codec/CodecSupport.java +++ b/lang/src/main/java/org/apache/shiro/lang/codec/CodecSupport.java @@ -20,7 +20,13 @@ import org.apache.shiro.lang.util.ByteSource; -import java.io.*; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; /** * Base abstract class that provides useful encoding and decoding operations, especially for character data. @@ -188,28 +194,28 @@ protected boolean isByteSource(Object o) { * If the argument is anything other than these types, it is passed to the * {@link #objectToBytes(Object) objectToBytes} method which must be overridden by subclasses. * - * @param o the Object to convert into a byte array + * @param object the Object to convert into a byte array * @return a byte array representation of the Object argument. */ - protected byte[] toBytes(Object o) { - if (o == null) { + protected byte[] toBytes(Object object) { + if (object == null) { String msg = "Argument for byte conversion cannot be null."; throw new IllegalArgumentException(msg); } - if (o instanceof byte[]) { - return (byte[]) o; - } else if (o instanceof ByteSource) { - return ((ByteSource) o).getBytes(); - } else if (o instanceof char[]) { - return toBytes((char[]) o); - } else if (o instanceof String) { - return toBytes((String) o); - } else if (o instanceof File) { - return toBytes((File) o); - } else if (o instanceof InputStream) { - return toBytes((InputStream) o); + if (object instanceof byte[]) { + return (byte[]) object; + } else if (object instanceof ByteSource) { + return ((ByteSource) object).getBytes(); + } else if (object instanceof char[]) { + return toBytes((char[]) object); + } else if (object instanceof String) { + return toBytes((String) object); + } else if (object instanceof File) { + return toBytes((File) object); + } else if (object instanceof InputStream) { + return toBytes((InputStream) object); } else { - return objectToBytes(o); + return objectToBytes(object); } } diff --git a/lang/src/main/java/org/apache/shiro/lang/util/SimpleByteSource.java b/lang/src/main/java/org/apache/shiro/lang/util/SimpleByteSource.java index 18594f6f01..dbb8d3d3c1 100644 --- a/lang/src/main/java/org/apache/shiro/lang/util/SimpleByteSource.java +++ b/lang/src/main/java/org/apache/shiro/lang/util/SimpleByteSource.java @@ -130,14 +130,21 @@ public static boolean isCompatible(Object o) { o instanceof ByteSource || o instanceof File || o instanceof InputStream; } + public static ByteSource empty() { + return new SimpleByteSource(new byte[]{}); + } + + @Override public byte[] getBytes() { - return this.bytes; + return Arrays.copyOf(this.bytes, this.bytes.length); } + @Override public boolean isEmpty() { return this.bytes == null || this.bytes.length == 0; } + @Override public String toHex() { if ( this.cachedHex == null ) { this.cachedHex = Hex.encodeToString(getBytes()); @@ -145,6 +152,7 @@ public String toHex() { return this.cachedHex; } + @Override public String toBase64() { if ( this.cachedBase64 == null ) { this.cachedBase64 = Base64.encodeToString(getBytes()); @@ -152,10 +160,12 @@ public String toBase64() { return this.cachedBase64; } + @Override public String toString() { return toBase64(); } + @Override public int hashCode() { if (this.bytes == null || this.bytes.length == 0) { return 0; @@ -163,6 +173,7 @@ public int hashCode() { return Arrays.hashCode(this.bytes); } + @Override public boolean equals(Object o) { if (o == this) { return true; diff --git a/lang/src/main/java/org/apache/shiro/lang/util/StringUtils.java b/lang/src/main/java/org/apache/shiro/lang/util/StringUtils.java index 4ae65c91be..b748d32625 100644 --- a/lang/src/main/java/org/apache/shiro/lang/util/StringUtils.java +++ b/lang/src/main/java/org/apache/shiro/lang/util/StringUtils.java @@ -322,6 +322,18 @@ public static String[] splitKeyValue(String aLine) throws ParseException { return split; } + /** + * Splits a string using the {@link #DEFAULT_DELIMITER_CHAR} (which is {@value #DEFAULT_DELIMITER_CHAR}). + * This method also recognizes quoting using the {@link #DEFAULT_QUOTE_CHAR} + * (which is {@value #DEFAULT_QUOTE_CHAR}), but does not retain them. + * + *

    This is equivalent of calling {@link #split(String, char, char, char, boolean, boolean)} with + * {@code line, DEFAULT_DELIMITER_CHAR, DEFAULT_QUOTE_CHAR, DEFAULT_QUOTE_CHAR, false, true}.

    + * + * @param line the line to split using the {@link #DEFAULT_DELIMITER_CHAR}. + * @return the split line, split tokens do not contain quotes and are trimmed. + * @see #split(String, char, char, char, boolean, boolean) + */ public static String[] split(String line) { return split(line, DEFAULT_DELIMITER_CHAR); } diff --git a/pom.xml b/pom.xml index 41d098192e..1ca995bef6 100644 --- a/pom.xml +++ b/pom.xml @@ -111,6 +111,7 @@ 4.2.2 2.1.6 2.39.0 + 1.68 4.0.2 @@ -750,6 +751,16 @@ shiro-crypto-hash ${project.version} + + org.apache.shiro.crypto + shiro-hashes-argon2 + ${project.version} + + + org.apache.shiro.crypto + shiro-hashes-bcrypt + ${project.version} + org.apache.shiro shiro-crypto-cipher @@ -1223,6 +1234,12 @@ junit-servers-jetty ${junit.server.jetty.version} + + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + diff --git a/tools/hasher/pom.xml b/tools/hasher/pom.xml index 9af02f84b7..2a3b4aa332 100644 --- a/tools/hasher/pom.xml +++ b/tools/hasher/pom.xml @@ -44,13 +44,28 @@ + + + org.apache.shiro.crypto + shiro-hashes-argon2 + compile + + + org.apache.shiro.crypto + shiro-hashes-bcrypt + compile + commons-cli commons-cli org.slf4j - slf4j-simple + slf4j-api + + + ch.qos.logback + logback-classic runtime diff --git a/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java b/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java index e20315303a..020d6d4298 100644 --- a/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java +++ b/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java @@ -20,13 +20,11 @@ import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; -import org.apache.commons.cli.DefaultParser; import org.apache.shiro.authc.credential.DefaultPasswordService; -import org.apache.shiro.lang.codec.Base64; -import org.apache.shiro.lang.codec.Hex; import org.apache.shiro.crypto.SecureRandomNumberGenerator; import org.apache.shiro.crypto.UnknownAlgorithmException; import org.apache.shiro.crypto.hash.DefaultHashService; @@ -37,15 +35,24 @@ import org.apache.shiro.crypto.hash.format.HashFormat; import org.apache.shiro.crypto.hash.format.HashFormatFactory; import org.apache.shiro.crypto.hash.format.HexFormat; -import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat; +import org.apache.shiro.crypto.hash.format.Shiro2CryptFormat; +import org.apache.shiro.crypto.support.hashes.argon2.Argon2HashProvider; +import org.apache.shiro.lang.codec.Base64; +import org.apache.shiro.lang.codec.Hex; import org.apache.shiro.lang.io.ResourceUtils; import org.apache.shiro.lang.util.ByteSource; import org.apache.shiro.lang.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStreamReader; import java.util.Arrays; +import static java.util.Collections.emptyMap; + /** * Commandline line utility to hash data such as strings, passwords, resources (files, urls, etc). *

    @@ -59,16 +66,18 @@ */ public final class Hasher { + private static final Logger LOG = LoggerFactory.getLogger(Hasher.class); + private static final String HEX_PREFIX = "0x"; private static final String DEFAULT_ALGORITHM_NAME = "MD5"; private static final String DEFAULT_PASSWORD_ALGORITHM_NAME = DefaultPasswordService.DEFAULT_HASH_ALGORITHM; private static final int DEFAULT_GENERATED_SALT_SIZE = 128; private static final int DEFAULT_NUM_ITERATIONS = 1; - private static final int DEFAULT_PASSWORD_NUM_ITERATIONS = DefaultPasswordService.DEFAULT_HASH_ITERATIONS; + private static final int DEFAULT_PASSWORD_NUM_ITERATIONS = Argon2HashProvider.Parameters.DEFAULT_ITERATIONS; - private static final Option ALGORITHM = new Option("a", "algorithm", true, "hash algorithm name. Defaults to SHA-256 when password hashing, MD5 otherwise."); + private static final Option ALGORITHM = new Option("a", "algorithm", true, "hash algorithm name. Defaults to Argon2 when password hashing, SHA-512 otherwise."); private static final Option DEBUG = new Option("d", "debug", false, "show additional error (stack trace) information."); - private static final Option FORMAT = new Option("f", "format", true, "hash output format. Defaults to 'shiro1' when password hashing, 'hex' otherwise. See below for more information."); + private static final Option FORMAT = new Option("f", "format", true, "hash output format. Defaults to 'shiro2' when password hashing, 'hex' otherwise. See below for more information."); private static final Option HELP = new Option("help", "help", false, "show this help message."); private static final Option ITERATIONS = new Option("i", "iterations", true, "number of hash iterations. Defaults to " + DEFAULT_PASSWORD_NUM_ITERATIONS + " when password hashing, 1 otherwise."); private static final Option PASSWORD = new Option("p", "password", false, "hash a password (disable typing echo)"); @@ -223,18 +232,17 @@ public static void main(String[] args) { } ByteSource publicSalt = getSalt(saltString, saltBytesString, generateSalt, generatedSaltSize); - ByteSource privateSalt = getSalt(privateSaltString, privateSaltBytesString, false, generatedSaltSize); - HashRequest hashRequest = new SimpleHashRequest(algorithm, ByteSource.Util.bytes(source), publicSalt, iterations); + // FIXME: add options here. + HashRequest hashRequest = new SimpleHashRequest(algorithm, ByteSource.Util.bytes(source), publicSalt, emptyMap()); DefaultHashService hashService = new DefaultHashService(); - hashService.setPrivateSalt(privateSalt); Hash hash = hashService.computeHash(hashRequest); if (formatString == null) { - //Output format was not specified. Default to 'shiro1' when password hashing, and 'hex' for + //Output format was not specified. Default to 'shiro2' when password hashing, and 'hex' for //everything else: if (password) { - formatString = Shiro1CryptFormat.class.getName(); + formatString = Shiro2CryptFormat.class.getName(); } else { formatString = HexFormat.class.getName(); } @@ -248,7 +256,7 @@ public static void main(String[] args) { String output = format.format(hash); - System.out.println(output); + LOG.info(output); } catch (IllegalArgumentException iae) { exit(iae, debug); @@ -339,16 +347,16 @@ private static ByteSource getSalt(String saltString, String saltBytesString, boo private static void printException(Exception e, boolean debug) { if (e != null) { - System.out.println(); + LOG.info(""); if (debug) { - System.out.println("Error: "); + LOG.info("Error: "); e.printStackTrace(System.out); - System.out.println(e.getMessage()); + LOG.info(e.getMessage()); } else { - System.out.println("Error: " + e.getMessage()); - System.out.println(); - System.out.println("Specify -d or --debug for more information."); + LOG.info("Error: " + e.getMessage()); + LOG.info(""); + LOG.info("Specify -d or --debug for more information."); } } } @@ -388,7 +396,7 @@ private static void printHelp(Options options, Exception e, boolean debug) { "a positive integer (size is in bits, not bytes)." + "\n\n" + "Because a salt must be specified if computing the hash later,\n" + - "generated salts are only useful with the shiro1 output format;\n" + + "generated salts are only useful with the shiro1/shiro2 output format;\n" + "the other formats do not include the generated salt." + "\n\n" + "Specifying a private salt:" + @@ -424,16 +432,16 @@ private static void printHelp(Options options, Exception e, boolean debug) { "by the " + DefaultHashFormatFactory.class.getName() + "\n" + "JavaDoc) or 2) the fully qualified " + HashFormat.class.getName() + "\n" + "implementation class name to instantiate and use for formatting.\n\n" + - "The default output format is 'shiro1' which is a Modular Crypt Format (MCF)\n" + + "The default output format is 'shiro2' which is a Modular Crypt Format (MCF)\n" + "that shows all relevant information as a dollar-sign ($) delimited string.\n" + "This format is ideal for use in Shiro's text-based user configuration (e.g.\n" + "shiro.ini or a properties file)."; printException(e, debug); - System.out.println(); + LOG.info(""); help.printHelp(command, header, options, null); - System.out.println(footer); + LOG.info(footer); } private static void printHelpAndExit(Options options, Exception e, boolean debug, int exitCode) { @@ -441,12 +449,20 @@ private static void printHelpAndExit(Options options, Exception e, boolean debug System.exit(exitCode); } - private static char[] readPassword(boolean confirm) { + private static char[] readPassword(boolean confirm) throws IOException { java.io.Console console = System.console(); - if (console == null) { - throw new IllegalStateException("java.io.Console is not available on the current JVM. Cannot read passwords."); + char[] first; + if (console != null) { + first = console.readPassword("%s", "Password to hash: "); + //throw new IllegalStateException("java.io.Console is not available on the current JVM. Cannot read passwords."); + } else if (System.in != null) { + BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + String readLine = br.readLine(); + first = readLine.toCharArray(); + } else { + throw new IllegalStateException("java.io.Console and java.lang.System.in are not available on the current JVM. Cannot read passwords."); } - char[] first = console.readPassword("%s", "Password to hash: "); + if (first == null || first.length == 0) { throw new IllegalArgumentException("No password specified."); } diff --git a/tools/hasher/src/main/resources/logback.xml b/tools/hasher/src/main/resources/logback.xml new file mode 100644 index 0000000000..502d9d162f --- /dev/null +++ b/tools/hasher/src/main/resources/logback.xml @@ -0,0 +1,31 @@ + + + + + + + [%-5level] %msg%n + + + + + + + diff --git a/tools/hasher/src/test/java/org/apache/shiro/tools/hasher/HasherTest.java b/tools/hasher/src/test/java/org/apache/shiro/tools/hasher/HasherTest.java new file mode 100644 index 0000000000..00e6286027 --- /dev/null +++ b/tools/hasher/src/test/java/org/apache/shiro/tools/hasher/HasherTest.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.shiro.tools.hasher; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @since 2.0 + */ +public class HasherTest { + + private final InputStream systemIn = System.in; + + private ByteArrayInputStream testIn; + + private final Logger hasherToolLogger = (Logger) LoggerFactory.getLogger("ROOT"); + private final ListAppender listAppender = new ListAppender<>(); + + @BeforeEach + public void setUpOutput() { + hasherToolLogger.detachAndStopAllAppenders(); + hasherToolLogger.addAppender(listAppender); + listAppender.start(); + } + + private void provideInput(String data) { + testIn = new ByteArrayInputStream(data.getBytes()); + System.setIn(testIn); + } + + @AfterEach + public void restoreSystemInputOutput() throws IOException { + System.setIn(systemIn); + testIn.close(); + listAppender.stop(); + } + + + @Test + public void testArgon2Hash() { + // given + String[] args = {"--debug", "--password", "--pnoconfirm"}; + provideInput("secret#shiro,password;Jo8opech"); + + // when + Hasher.main(args); + List loggingEvents = listAppender.list; + + // when + assertEquals(1, loggingEvents.size()); + ILoggingEvent iLoggingEvent = loggingEvents.get(0); + assertTrue(iLoggingEvent.getMessage().contains("$shiro2$argon2id$v=19")); + } + + @Test + public void testBCryptHash() { + // given + String[] args = {"--debug", "--password", "--pnoconfirm", "--algorithm", "2y"}; + provideInput("secret#shiro,password;Jo8opech"); + + // when + Hasher.main(args); + List loggingEvents = listAppender.list; + + // when + assertEquals(1, loggingEvents.size()); + ILoggingEvent iLoggingEvent = loggingEvents.get(0); + assertTrue(iLoggingEvent.getMessage().contains("$shiro2$2y$10$")); + } +} diff --git a/tools/hasher/src/test/resources/logback-test.xml b/tools/hasher/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..a6523923f2 --- /dev/null +++ b/tools/hasher/src/test/resources/logback-test.xml @@ -0,0 +1,28 @@ + + + + + + + + + + +