add calculation of bcryt strength with Divide-and-conquer algorithm

This commit is contained in:
akuksin
2020-02-22 23:35:21 +01:00
parent 2ae9c0b2ae
commit 3b69be714e
29 changed files with 545 additions and 516 deletions

View File

@@ -9,5 +9,4 @@ public class PasswordEncodingSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(PasswordEncodingSpringBootApplication.class, args);
}
}

View File

@@ -13,12 +13,12 @@ public class JdbcUserDetailPasswordService implements UserDetailsPasswordService
private final UserDetailsMapper userDetailsMapper;
public JdbcUserDetailPasswordService(UserRepository userRepository, UserDetailsMapper userDetailsMapper) {
public JdbcUserDetailPasswordService(
UserRepository userRepository, UserDetailsMapper userDetailsMapper) {
this.userRepository = userRepository;
this.userDetailsMapper = userDetailsMapper;
}
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
UserCredentials userCredentials = userRepository.findByUsername(user.getUsername());

View File

@@ -14,12 +14,12 @@ public class JdbcUserDetailsService implements UserDetailsService {
private final UserDetailsMapper userDetailsMapper;
public JdbcUserDetailsService(UserRepository userRepository, UserDetailsMapper userDetailsMapper) {
public JdbcUserDetailsService(
UserRepository userRepository, UserDetailsMapper userDetailsMapper) {
this.userRepository = userRepository;
this.userDetailsMapper = userDetailsMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserCredentials userCredentials = userRepository.findByUsername(username);

View File

@@ -16,16 +16,16 @@ import java.util.Set;
@Table(name = "users")
public class UserCredentials {
@Id
private String username;
@Id private String username;
private String password;
boolean enabled;
@ElementCollection
@JoinTable(name = "authorities", joinColumns = {@JoinColumn(name = "username")})
@JoinTable(
name = "authorities",
joinColumns = {@JoinColumn(name = "username")})
@Column(name = "authority")
private Set<String> roles;
}

View File

@@ -9,8 +9,7 @@ public class UserDetailsMapper {
UserDetails toUserDetails(UserCredentials userCredentials) {
return User
.withUsername(userCredentials.getUsername())
return User.withUsername(userCredentials.getUsername())
.password(userCredentials.getPassword())
.roles(userCredentials.getRoles().toArray(String[]::new))
.build();

View File

@@ -29,12 +29,14 @@ import java.util.Map;
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final UserRepository userRepository;
private final UserDetailsMapper userDetailsMapper;
private final BcCryptWorkFactorService bcCryptWorkFactorService;
public SecurityConfiguration(UserRepository userRepository, UserDetailsMapper userDetailsMapper, BcCryptWorkFactorService bcCryptWorkFactorService) {
public SecurityConfiguration(
UserRepository userRepository,
UserDetailsMapper userDetailsMapper,
BcCryptWorkFactorService bcCryptWorkFactorService) {
this.userRepository = userRepository;
this.userDetailsMapper = userDetailsMapper;
this.bcCryptWorkFactorService = bcCryptWorkFactorService;
@@ -43,10 +45,13 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/registration").permitAll()
.anyRequest().authenticated()
.antMatchers("/registration")
.permitAll()
.anyRequest()
.authenticated()
.and()
.httpBasic();
@@ -55,27 +60,33 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(daoAuthenticationProvider())
.eraseCredentials(false);
auth.authenticationProvider(daoAuthenticationProvider()).eraseCredentials(false);
}
@Bean
public PasswordEncoder passwordEncoder() {
// we must user deprecated encoder to support their encoding
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder(bcCryptWorkFactorService.calculateStrength()));
encoders.put(
encodingId, new BCryptPasswordEncoder(bcCryptWorkFactorService.calculateStrength()));
encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
encoders.put(
"MD5",
new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
encoders.put(
"noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
encoders.put(
"SHA-1",
new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
encoders.put(
"SHA-256",
new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
encoders.put(
"sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);

View File

@@ -4,7 +4,6 @@ import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
public class Argon2Example {
public String encode(String plainPassword) {
int saltLength = 16; // salt length in bytes
int hashLength = 32; // hash length in bytes
@@ -12,7 +11,8 @@ public class Argon2Example {
int memory = 4096; // memory costs
int iterations = 3;
Argon2PasswordEncoder argon2PasswordEncoder = new Argon2PasswordEncoder(saltLength, hashLength, parallelism, memory, iterations);
Argon2PasswordEncoder argon2PasswordEncoder =
new Argon2PasswordEncoder(saltLength, hashLength, parallelism, memory, iterations);
return argon2PasswordEncoder.encode(plainPassword);
}
}

View File

@@ -8,7 +8,8 @@ public class BCryptExample {
public String encode(String plainPassword) {
int strength = 10;
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(strength, new SecureRandom());
BCryptPasswordEncoder bCryptPasswordEncoder =
new BCryptPasswordEncoder(strength, new SecureRandom());
return bCryptPasswordEncoder.encode(plainPassword);
}
}

View File

@@ -10,7 +10,8 @@ public class Pbkdf2Example {
int iterations = 200000; // number of hash iteration
int hashWidth = 256; // hash with in bits
Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder(pepper, iterations, hashWidth);
Pbkdf2PasswordEncoder pbkdf2PasswordEncoder =
new Pbkdf2PasswordEncoder(pepper, iterations, hashWidth);
return pbkdf2PasswordEncoder.encode(plainPassword);
}
}

View File

@@ -11,7 +11,8 @@ public class SCryptExample {
int keyLength = 32; // key length in bytes
int saltLength = 64; // salt length in bytes
SCryptPasswordEncoder sCryptPasswordEncoder = new SCryptPasswordEncoder(cpuCost, memoryCost, parallelization, keyLength, saltLength);
SCryptPasswordEncoder sCryptPasswordEncoder =
new SCryptPasswordEncoder(cpuCost, memoryCost, parallelization, keyLength, saltLength);
return sCryptPasswordEncoder.encode(plainPassword);
}
}

View File

@@ -15,8 +15,7 @@ public class PasswordMigration {
@Bean
public ApplicationListener<AuthenticationSuccessEvent> authenticationSuccessListener(
PasswordEncoder encoder,
UserDetailsPasswordService userDetailsPasswordService) {
PasswordEncoder encoder, UserDetailsPasswordService userDetailsPasswordService) {
return (AuthenticationSuccessEvent event) -> {
Authentication authentication = event.getAuthentication();
User user = (User) authentication.getPrincipal();
@@ -29,5 +28,4 @@ public class PasswordMigration {
((UsernamePasswordAuthenticationToken) authentication).eraseCredentials();
};
}
}

View File

@@ -12,15 +12,7 @@ public class CarResources {
@GetMapping("/cars")
public Set<Car> cars() {
return Set.of(
Car.builder()
.name("vw")
.color("black")
.build(),
Car.builder()
.name("bmw")
.color("white")
.build()
);
Car.builder().name("vw").color("black").build(),
Car.builder().name("bmw").color("white").build());
}
}

View File

@@ -24,11 +24,11 @@ public class UserResources {
this.passwordEncoder = passwordEncoder;
}
@PostMapping("/registration")
@ResponseStatus(code = HttpStatus.CREATED)
public void register(@RequestBody UserCredentialsDto userCredentialsDto) {
UserCredentials user = UserCredentials.builder()
UserCredentials user =
UserCredentials.builder()
.enabled(true)
.username(userCredentialsDto.getUsername())
.password(passwordEncoder.encode(userCredentialsDto.getPassword()))

View File

@@ -14,62 +14,74 @@ public class BcCryptWorkFactorService {
private static final int MIN_STRENGTH = 4;
private static final int MAX_STRENGTH = 31;
/**
* Calculates the strength (a.k.a. log rounds) for the BCrypt Algorithm, so that password encoding
* takes about 1s. This method uses the divide-and-conquer algorithm.
*/
public BcryptWorkFactor calculateStrengthDivideAndConquer() {
return calculateStrengthDivideAndConquer(
new BcryptWorkFactor(MIN_STRENGTH, Integer.MIN_VALUE),
new BcryptWorkFactor(MAX_STRENGTH, Integer.MAX_VALUE));
}
private BcryptWorkFactor calculateStrengthDivideAndConquer(
BcryptWorkFactor smallFactor, BcryptWorkFactor bigFactor) {
if (bigFactor.getStrength() - smallFactor.getStrength() == 1) {
return getClosestStrength(smallFactor, bigFactor);
}
int midStrength =
(bigFactor.getStrength() - smallFactor.getStrength()) / 2 + smallFactor.getStrength();
long duration = calculateDuration(midStrength);
BcryptWorkFactor midFactor = new BcryptWorkFactor(midStrength, duration);
if (duration < GOAL_MILLISECONDS_PER_PASSWORD) {
return calculateStrengthDivideAndConquer(midFactor, bigFactor);
}
return calculateStrengthDivideAndConquer(smallFactor, midFactor);
}
private BcryptWorkFactor getClosestStrength(
BcryptWorkFactor smallFactor, BcryptWorkFactor bigFactor) {
if (isPreviousDurationCloserToGoal(smallFactor.getDuration(), bigFactor.getDuration())) {
return smallFactor;
}
return bigFactor;
}
private long calculateDuration(int strength) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(strength);
Stopwatch stopwatch = Stopwatch.createStarted();
bCryptPasswordEncoder.encode(TEST_PASSWORD);
stopwatch.stop();
return stopwatch.elapsed(TimeUnit.MILLISECONDS);
}
/**
* Calculates the strength (a.k.a. log rounds) for the BCrypt Algorithm, so that password encoding takes about 1s.
* This method iterates over strength from 4 to 31 and calculates the duration of password encoding for every value of strength.
* It returns the first strength, that takes more than 500s
* Calculates the strength (a.k.a. log rounds) for the BCrypt Algorithm, so that password encoding
* takes about 1s. This method iterates over strength from 4 to 31 and calculates the duration of
* password encoding for every value of strength. It returns the first strength, that takes more
* than 1s
*/
public int calculateStrength() {
for (int strength = MIN_STRENGTH; strength <= MAX_STRENGTH; strength++) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(strength);
Stopwatch stopwatch = Stopwatch.createStarted();
bCryptPasswordEncoder.encode(TEST_PASSWORD);
stopwatch.stop();
long duration = stopwatch.elapsed(TimeUnit.MILLISECONDS);
long duration = calculateDuration(strength);
if (duration >= GOAL_MILLISECONDS_PER_PASSWORD) {
return strength;
}
}
throw new RuntimeException(String.format("Could not find suitable round number for bcrypt encoding. The encoding with %d rounds" +
" takes less than %d ms.", MAX_STRENGTH, GOAL_MILLISECONDS_PER_PASSWORD));
}
/**
* Calculates the strength (a.k.a. log rounds) for the BCrypt Algorithm, so that password encoding takes about 1s.
* This method iterate over strength from 4 to 31 and calculates the duration of password encoding for every value of strength.
* When the the duration takes more than 1s, it is compared to previous one and the method returns the strength, tha is closer
* to 1s.
*/
public int calculateStrengthClosestToTimeGoal() {
long previousDuration = Long.MIN_VALUE;
for (int strength = MIN_STRENGTH; strength <= MAX_STRENGTH; strength++) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(strength);
Stopwatch stopwatch = Stopwatch.createStarted();
bCryptPasswordEncoder.encode(TEST_PASSWORD);
stopwatch.stop();
long currentDuration = stopwatch.elapsed(TimeUnit.MILLISECONDS);
if (isGreaterThanGoal(currentDuration)) {
return getStrength(previousDuration, currentDuration, strength);
}
previousDuration = currentDuration;
}
throw new RuntimeException(String.format("Could not find suitable round number for bcrypt encoding. The encoding with %d rounds" +
" takes less than %d ms.", MAX_STRENGTH, GOAL_MILLISECONDS_PER_PASSWORD));
throw new RuntimeException(
String.format(
"Could not find suitable round number for bcrypt encoding. The encoding with %d rounds"
+ " takes less than %d ms.",
MAX_STRENGTH, GOAL_MILLISECONDS_PER_PASSWORD));
}
/**
* @param previousDuration duration from previous iteration
* @param currentDuration duration of current iteration
* @param strength current strength
* @return return the current strength, if current duration is closer to GOAL_MILLISECONDS_PER_PASSWORD, otherwise
* current strength-1.
* @return return the current strength, if current duration is closer to
* GOAL_MILLISECONDS_PER_PASSWORD, otherwise current strength-1.
*/
int getStrength(long previousDuration, long currentDuration, int strength) {
if (isPreviousDurationCloserToGoal(previousDuration, currentDuration)) {
@@ -79,14 +91,11 @@ public class BcCryptWorkFactorService {
}
}
private boolean isGreaterThanGoal(long duration) {
return duration > GOAL_MILLISECONDS_PER_PASSWORD;
}
/**
* return true, if previousDuration is closer to the goal than currentDuration, false otherwise.
*/
boolean isPreviousDurationCloserToGoal(long previousDuration, long currentDuration) {
return Math.abs(GOAL_MILLISECONDS_PER_PASSWORD - previousDuration) < Math.abs(GOAL_MILLISECONDS_PER_PASSWORD - currentDuration);
return Math.abs(GOAL_MILLISECONDS_PER_PASSWORD - previousDuration)
< Math.abs(GOAL_MILLISECONDS_PER_PASSWORD - currentDuration);
}
}

View File

@@ -0,0 +1,12 @@
package io.reflectoring.passwordencoding.workfactor;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class BcryptWorkFactor {
private int strength;
private long duration;
}

View File

@@ -16,14 +16,16 @@ public class Pbkdf2WorkFactorService {
private static final int ITERATION_STEP = 5000;
/**
* Finds the number of Iteration for the {@link Pbkdf2PasswordEncoder} to get the duration of password encoding
* close to 1s. The Calculation does not use any secret (pepper) and applies hash algorithm SHA256.
* Finds the number of Iteration for the {@link Pbkdf2PasswordEncoder} to get the duration of
* password encoding close to 1s. The Calculation does not use any secret (pepper) and applies
* hash algorithm SHA256.
*/
public int calculateIteration() {
int iterationNumber = 150000;
while (true) {
Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder(NO_ADDITIONAL_SECRET, iterationNumber, HASH_WIDTH);
Pbkdf2PasswordEncoder pbkdf2PasswordEncoder =
new Pbkdf2PasswordEncoder(NO_ADDITIONAL_SECRET, iterationNumber, HASH_WIDTH);
Stopwatch stopwatch = Stopwatch.createStarted();
pbkdf2PasswordEncoder.encode(TEST_PASSWORD);

View File

@@ -1,5 +1,15 @@
insert into users (username, password, enabled) VALUES ('admin', '{bcrypt}$2a$10$4V9kA793Pi2xf94dYFgKWuw8ukyETxWb7tZ4/mfco9sWkwvBQndxW', true);
insert into users (username, password, enabled) VALUES ('user', '{SHA-256}{4Cc0+yDMHnTUy+zOHeMH7yaPhxvlJT//tQTwEhyegiQ=}446d06130bfc254527a7bbd95b50595a977c0058110f8dccb54bd273d99325b8', true);
insert into users (username, password, enabled) VALUES ('user with working factor 5', '{bcrypt}$2a$05$Zz4rToG8YXKMbuAPgm3qj.HpTFsGEdZHhCf9ikIHAoI5elX7ajNm.', true);
insert into users (username, password, enabled) VALUES ('user with sha1 encoding', '{SHA-1}{6tND0AZfFH3aE1VDg7QkWT6DzFg/NUHtukntgwu8JV4=}804c6e8efebf4e91f88e3baf9fd383e28a21378c', true);
insert into users (username, password, enabled) VALUES ('scrypt user', '{scrypt}$e0801$fUx3MxN07zdH3UyARJqOwv3WiWCvE7f6qRm9A5KQfNo5ovSwxMHknQ4vERO4csj/I3imG2HJQg1HHp7Rqzbp7g==$Fm5F9PSoE/jBYLOmnCJcvX1Euf952r5b3BjAl+SwQMs=', true);
insert into users (username, password, enabled)
VALUES ('admin', '{bcrypt}$2a$10$4V9kA793Pi2xf94dYFgKWuw8ukyETxWb7tZ4/mfco9sWkwvBQndxW', true);
insert into users (username, password, enabled)
VALUES ('user',
'{SHA-256}{4Cc0+yDMHnTUy+zOHeMH7yaPhxvlJT//tQTwEhyegiQ=}446d06130bfc254527a7bbd95b50595a977c0058110f8dccb54bd273d99325b8',
true);
insert into users (username, password, enabled)
VALUES ('user with working factor 5', '{bcrypt}$2a$05$Zz4rToG8YXKMbuAPgm3qj.HpTFsGEdZHhCf9ikIHAoI5elX7ajNm.', true);
insert into users (username, password, enabled)
VALUES ('user with sha1 encoding',
'{SHA-1}{6tND0AZfFH3aE1VDg7QkWT6DzFg/NUHtukntgwu8JV4=}804c6e8efebf4e91f88e3baf9fd383e28a21378c', true);
insert into users (username, password, enabled)
VALUES ('scrypt user',
'{scrypt}$e0801$fUx3MxN07zdH3UyARJqOwv3WiWCvE7f6qRm9A5KQfNo5ovSwxMHknQ4vERO4csj/I3imG2HJQg1HHp7Rqzbp7g==$Fm5F9PSoE/jBYLOmnCJcvX1Euf952r5b3BjAl+SwQMs=',
true);

View File

@@ -14,7 +14,8 @@ class UserDetailsMapperTest {
@Test
void toUserDetails() {
// given
UserCredentials userCredentials = UserCredentials.builder()
UserCredentials userCredentials =
UserCredentials.builder()
.enabled(true)
.password("password")
.username("user")

View File

@@ -9,8 +9,7 @@ import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired private UserRepository userRepository;
@Test
void findUserByUsername() {

View File

@@ -22,34 +22,31 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@Transactional
class CarResourcesTest {
@Autowired
private MockMvc mockMvc;
@Autowired private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired private ObjectMapper objectMapper;
@Autowired
private UserRepository userRepository;
@Autowired private UserRepository userRepository;
@Test
void getCarsShouldReturnUnauthorizedIfTheRequestHasNoBasicAuthentication() throws Exception {
mockMvc.perform(get("/cars"))
.andExpect(status().isUnauthorized());
mockMvc.perform(get("/cars")).andExpect(status().isUnauthorized());
}
@Test
void getCarsShouldReturnCarsForTheAuthenticatedUser() throws Exception {
mockMvc.perform(get("/cars")
.with(httpBasic("user", "password")))
.andExpect(status().isOk());
mockMvc.perform(get("/cars").with(httpBasic("user", "password"))).andExpect(status().isOk());
}
@Test
void registrationShouldReturnCreated() throws Exception {
// register
UserCredentialsDto userCredentialsDto = UserCredentialsDto.builder().username("toyota").password("my secret").build();
mockMvc.perform(post("/registration")
UserCredentialsDto userCredentialsDto =
UserCredentialsDto.builder().username("toyota").password("my secret").build();
mockMvc
.perform(
post("/registration")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(userCredentialsDto)))
.andExpect(status().isCreated());
@@ -58,15 +55,15 @@ class CarResourcesTest {
@Test
void registrationShouldReturnUnauthorizedWithWrongCredentials() throws Exception {
mockMvc.perform(get("/cars")
.with(httpBasic("user", "wrong password")))
mockMvc
.perform(get("/cars").with(httpBasic("user", "wrong password")))
.andExpect(status().isUnauthorized());
}
@Test
void getCarsShouldUpdatePasswordFromWorkingFactor5toHigherValue() throws Exception {
mockMvc.perform(get("/cars")
.with(httpBasic("user with working factor 5", "password")))
mockMvc
.perform(get("/cars").with(httpBasic("user with working factor 5", "password")))
.andExpect(status().isOk());
UserCredentials userCredentials = userRepository.findByUsername("user with working factor 5");
@@ -77,8 +74,8 @@ class CarResourcesTest {
@Test
void getCarsShouldUpdateSha1PasswordToBcrypt() throws Exception {
mockMvc.perform(get("/cars")
.with(httpBasic("user with sha1 encoding", "password")))
mockMvc
.perform(get("/cars").with(httpBasic("user with sha1 encoding", "password")))
.andExpect(status().isOk());
UserCredentials userCredentials = userRepository.findByUsername("user with sha1 encoding");
@@ -87,8 +84,8 @@ class CarResourcesTest {
@Test
void getCarsShouldReturnOkForScryptUser() throws Exception {
mockMvc.perform(get("/cars")
.with(httpBasic("scrypt user", "password")))
mockMvc
.perform(get("/cars").with(httpBasic("scrypt user", "password")))
.andExpect(status().isOk());
}
}

View File

@@ -1,7 +1,6 @@
package io.reflectoring.passwordencoding.workfactor;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
@@ -16,21 +15,20 @@ class BcCryptWorkFactorServiceTest {
// when
int strength = bcCryptWorkFactorService.calculateStrength();
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(strength);
// then
assertThat(strength).isBetween(4, 31);
}
@Test
void calculateRounds() {
void calculateStrengthBi() {
// given
// when
int strength = bcCryptWorkFactorService.calculateStrengthClosestToTimeGoal();
BcryptWorkFactor bcryptWorkFactor =
bcCryptWorkFactorService.calculateStrengthDivideAndConquer();
// then
assertThat(strength).isBetween(4, 31);
assertThat(bcryptWorkFactor.getStrength()).isBetween(4, 31);
}
@Test

View File

@@ -6,7 +6,6 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
class Pbkdf2WorkFactorServiceTest {
private Pbkdf2WorkFactorService pbkdf2WorkFactorService = new Pbkdf2WorkFactorService();
@Test