|
| 1 | +/* |
| 2 | + * Copyright 2004-present the original author or authors. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * https://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +package org.springframework.security.crypto.password4j; |
| 18 | + |
| 19 | +import java.security.SecureRandom; |
| 20 | +import java.util.Base64; |
| 21 | + |
| 22 | +import com.password4j.AlgorithmFinder; |
| 23 | +import com.password4j.BalloonHashingFunction; |
| 24 | +import com.password4j.Hash; |
| 25 | +import com.password4j.Password; |
| 26 | + |
| 27 | +import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder; |
| 28 | +import org.springframework.util.Assert; |
| 29 | + |
| 30 | +/** |
| 31 | + * Implementation of {@link org.springframework.security.crypto.password.PasswordEncoder} |
| 32 | + * that uses the Password4j library with Balloon hashing algorithm. |
| 33 | + * |
| 34 | + * <p> |
| 35 | + * Balloon hashing is a memory-hard password hashing algorithm designed to be resistant to |
| 36 | + * both time-memory trade-off attacks and side-channel attacks. This implementation |
| 37 | + * handles the salt management explicitly since Password4j's Balloon hashing |
| 38 | + * implementation does not include the salt in the output hash. |
| 39 | + * </p> |
| 40 | + * |
| 41 | + * <p> |
| 42 | + * The encoded password format is: {salt}:{hash} where both salt and hash are Base64 |
| 43 | + * encoded. |
| 44 | + * </p> |
| 45 | + * |
| 46 | + * <p> |
| 47 | + * This implementation is thread-safe and can be shared across multiple threads. |
| 48 | + * </p> |
| 49 | + * |
| 50 | + * <p> |
| 51 | + * <strong>Usage Examples:</strong> |
| 52 | + * </p> |
| 53 | + * <pre>{@code |
| 54 | + * // Using default Balloon hashing settings (recommended) |
| 55 | + * PasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder(); |
| 56 | + * |
| 57 | + * // Using custom Balloon hashing function |
| 58 | + * PasswordEncoder customEncoder = new BalloonHashingPassword4jPasswordEncoder( |
| 59 | + * BalloonHashingFunction.getInstance(1024, 3, 4, "SHA-256")); |
| 60 | + * }</pre> |
| 61 | + * |
| 62 | + * @author Mehrdad Bozorgmehr |
| 63 | + * @since 7.0 |
| 64 | + * @see BalloonHashingFunction |
| 65 | + * @see AlgorithmFinder#getBalloonHashingInstance() |
| 66 | + */ |
| 67 | +public class BalloonHashingPassword4jPasswordEncoder extends AbstractValidatingPasswordEncoder { |
| 68 | + |
| 69 | + private static final String DELIMITER = ":"; |
| 70 | + |
| 71 | + private static final int DEFAULT_SALT_LENGTH = 32; |
| 72 | + |
| 73 | + private final BalloonHashingFunction balloonHashingFunction; |
| 74 | + |
| 75 | + private final SecureRandom secureRandom; |
| 76 | + |
| 77 | + private final int saltLength; |
| 78 | + |
| 79 | + /** |
| 80 | + * Constructs a Balloon hashing password encoder using the default Balloon hashing |
| 81 | + * configuration from Password4j's AlgorithmFinder. |
| 82 | + */ |
| 83 | + public BalloonHashingPassword4jPasswordEncoder() { |
| 84 | + this(AlgorithmFinder.getBalloonHashingInstance()); |
| 85 | + } |
| 86 | + |
| 87 | + /** |
| 88 | + * Constructs a Balloon hashing password encoder with a custom Balloon hashing |
| 89 | + * function. |
| 90 | + * @param balloonHashingFunction the Balloon hashing function to use for encoding |
| 91 | + * passwords, must not be null |
| 92 | + * @throws IllegalArgumentException if balloonHashingFunction is null |
| 93 | + */ |
| 94 | + public BalloonHashingPassword4jPasswordEncoder(BalloonHashingFunction balloonHashingFunction) { |
| 95 | + this(balloonHashingFunction, DEFAULT_SALT_LENGTH); |
| 96 | + } |
| 97 | + |
| 98 | + /** |
| 99 | + * Constructs a Balloon hashing password encoder with a custom Balloon hashing |
| 100 | + * function and salt length. |
| 101 | + * @param balloonHashingFunction the Balloon hashing function to use for encoding |
| 102 | + * passwords, must not be null |
| 103 | + * @param saltLength the length of the salt in bytes, must be positive |
| 104 | + * @throws IllegalArgumentException if balloonHashingFunction is null or saltLength is |
| 105 | + * not positive |
| 106 | + */ |
| 107 | + public BalloonHashingPassword4jPasswordEncoder(BalloonHashingFunction balloonHashingFunction, int saltLength) { |
| 108 | + Assert.notNull(balloonHashingFunction, "balloonHashingFunction cannot be null"); |
| 109 | + Assert.isTrue(saltLength > 0, "saltLength must be positive"); |
| 110 | + this.balloonHashingFunction = balloonHashingFunction; |
| 111 | + this.saltLength = saltLength; |
| 112 | + this.secureRandom = new SecureRandom(); |
| 113 | + } |
| 114 | + |
| 115 | + @Override |
| 116 | + protected String encodeNonNullPassword(String rawPassword) { |
| 117 | + byte[] salt = new byte[this.saltLength]; |
| 118 | + this.secureRandom.nextBytes(salt); |
| 119 | + |
| 120 | + Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.balloonHashingFunction); |
| 121 | + String encodedSalt = Base64.getEncoder().encodeToString(salt); |
| 122 | + String encodedHash = hash.getResult(); |
| 123 | + |
| 124 | + return encodedSalt + DELIMITER + encodedHash; |
| 125 | + } |
| 126 | + |
| 127 | + @Override |
| 128 | + protected boolean matchesNonNull(String rawPassword, String encodedPassword) { |
| 129 | + if (!encodedPassword.contains(DELIMITER)) { |
| 130 | + return false; |
| 131 | + } |
| 132 | + |
| 133 | + String[] parts = encodedPassword.split(DELIMITER, 2); |
| 134 | + if (parts.length != 2) { |
| 135 | + return false; |
| 136 | + } |
| 137 | + |
| 138 | + try { |
| 139 | + byte[] salt = Base64.getDecoder().decode(parts[0]); |
| 140 | + String expectedHash = parts[1]; |
| 141 | + |
| 142 | + Hash hash = Password.hash(rawPassword).addSalt(salt).with(this.balloonHashingFunction); |
| 143 | + return expectedHash.equals(hash.getResult()); |
| 144 | + } |
| 145 | + catch (IllegalArgumentException ex) { |
| 146 | + // Invalid Base64 encoding |
| 147 | + return false; |
| 148 | + } |
| 149 | + } |
| 150 | + |
| 151 | + @Override |
| 152 | + protected boolean upgradeEncodingNonNull(String encodedPassword) { |
| 153 | + // For now, we'll return false to maintain existing behavior |
| 154 | + // This could be enhanced in the future to check if the encoding parameters |
| 155 | + // match the current configuration |
| 156 | + return false; |
| 157 | + } |
| 158 | + |
| 159 | +} |
0 commit comments