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

@@ -6,8 +6,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication @SpringBootApplication
public class PasswordEncodingSpringBootApplication { public class PasswordEncodingSpringBootApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(PasswordEncodingSpringBootApplication.class, args); SpringApplication.run(PasswordEncodingSpringBootApplication.class, args);
} }
} }

View File

@@ -9,20 +9,20 @@ import org.springframework.transaction.annotation.Transactional;
@Service @Service
public class JdbcUserDetailPasswordService implements UserDetailsPasswordService { public class JdbcUserDetailPasswordService implements UserDetailsPasswordService {
private final UserRepository userRepository; private final UserRepository userRepository;
private final UserDetailsMapper userDetailsMapper; private final UserDetailsMapper userDetailsMapper;
public JdbcUserDetailPasswordService(UserRepository userRepository, UserDetailsMapper userDetailsMapper) { public JdbcUserDetailPasswordService(
this.userRepository = userRepository; UserRepository userRepository, UserDetailsMapper userDetailsMapper) {
this.userDetailsMapper = userDetailsMapper; this.userRepository = userRepository;
} this.userDetailsMapper = userDetailsMapper;
}
@Override
@Override public UserDetails updatePassword(UserDetails user, String newPassword) {
public UserDetails updatePassword(UserDetails user, String newPassword) { UserCredentials userCredentials = userRepository.findByUsername(user.getUsername());
UserCredentials userCredentials = userRepository.findByUsername(user.getUsername()); userCredentials.setPassword(newPassword);
userCredentials.setPassword(newPassword); return userDetailsMapper.toUserDetails(userCredentials);
return userDetailsMapper.toUserDetails(userCredentials); }
}
} }

View File

@@ -10,19 +10,19 @@ import org.springframework.transaction.annotation.Transactional;
@Transactional @Transactional
public class JdbcUserDetailsService implements UserDetailsService { public class JdbcUserDetailsService implements UserDetailsService {
private final UserRepository userRepository; private final UserRepository userRepository;
private final UserDetailsMapper userDetailsMapper; private final UserDetailsMapper userDetailsMapper;
public JdbcUserDetailsService(UserRepository userRepository, UserDetailsMapper userDetailsMapper) { public JdbcUserDetailsService(
this.userRepository = userRepository; UserRepository userRepository, UserDetailsMapper userDetailsMapper) {
this.userDetailsMapper = userDetailsMapper; this.userRepository = userRepository;
} this.userDetailsMapper = userDetailsMapper;
}
@Override
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserCredentials userCredentials = userRepository.findByUsername(username);
UserCredentials userCredentials = userRepository.findByUsername(username); return userDetailsMapper.toUserDetails(userCredentials);
return userDetailsMapper.toUserDetails(userCredentials); }
}
} }

View File

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

View File

@@ -7,12 +7,11 @@ import org.springframework.stereotype.Component;
@Component @Component
public class UserDetailsMapper { public class UserDetailsMapper {
UserDetails toUserDetails(UserCredentials userCredentials) { UserDetails toUserDetails(UserCredentials userCredentials) {
return User return User.withUsername(userCredentials.getUsername())
.withUsername(userCredentials.getUsername()) .password(userCredentials.getPassword())
.password(userCredentials.getPassword()) .roles(userCredentials.getRoles().toArray(String[]::new))
.roles(userCredentials.getRoles().toArray(String[]::new)) .build();
.build(); }
}
} }

View File

@@ -6,5 +6,5 @@ import org.springframework.transaction.annotation.Transactional;
@Transactional @Transactional
public interface UserRepository extends JpaRepository<UserCredentials, String> { public interface UserRepository extends JpaRepository<UserCredentials, String> {
UserCredentials findByUsername(String username); UserCredentials findByUsername(String username);
} }

View File

@@ -29,73 +29,84 @@ import java.util.Map;
@EnableWebSecurity @EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter { public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final UserRepository userRepository;
private final UserDetailsMapper userDetailsMapper;
private final BcCryptWorkFactorService bcCryptWorkFactorService;
private final UserRepository userRepository; public SecurityConfiguration(
private final UserDetailsMapper userDetailsMapper; UserRepository userRepository,
private final BcCryptWorkFactorService bcCryptWorkFactorService; UserDetailsMapper userDetailsMapper,
BcCryptWorkFactorService bcCryptWorkFactorService) {
this.userRepository = userRepository;
this.userDetailsMapper = userDetailsMapper;
this.bcCryptWorkFactorService = bcCryptWorkFactorService;
}
public SecurityConfiguration(UserRepository userRepository, UserDetailsMapper userDetailsMapper, BcCryptWorkFactorService bcCryptWorkFactorService) { @Override
this.userRepository = userRepository; protected void configure(HttpSecurity httpSecurity) throws Exception {
this.userDetailsMapper = userDetailsMapper; httpSecurity
this.bcCryptWorkFactorService = bcCryptWorkFactorService; .csrf()
} .disable()
.authorizeRequests()
.antMatchers("/registration")
.permitAll()
.anyRequest()
.authenticated()
.and()
.httpBasic();
@Override httpSecurity.headers().frameOptions().disable();
protected void configure(HttpSecurity httpSecurity) throws Exception { }
httpSecurity
.csrf().disable()
.authorizeRequests()
.antMatchers("/registration").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic();
httpSecurity.headers().frameOptions().disable(); @Autowired
} public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProvider()).eraseCredentials(false);
}
@Autowired @Bean
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { public PasswordEncoder passwordEncoder() {
auth // we must user deprecated encoder to support their encoding
.authenticationProvider(daoAuthenticationProvider()) String encodingId = "bcrypt";
.eraseCredentials(false); Map<String, PasswordEncoder> encoders = new HashMap<>();
} 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("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("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
@Bean @Bean
public PasswordEncoder passwordEncoder() { public UserDetailsPasswordService userDetailsPasswordService() {
// we must user deprecated encoder to support their encoding return new JdbcUserDetailPasswordService(userRepository, userDetailsMapper);
String encodingId = "bcrypt"; }
Map<String, PasswordEncoder> encoders = new HashMap<>();
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("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("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders); public UserDetailsService userDetailsService() {
} return new JdbcUserDetailsService(userRepository, userDetailsMapper);
}
@Bean @Bean
public UserDetailsPasswordService userDetailsPasswordService() { public DaoAuthenticationProvider daoAuthenticationProvider() {
return new JdbcUserDetailPasswordService(userRepository, userDetailsMapper); DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
} daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setUserDetailsPasswordService(userDetailsPasswordService());
public UserDetailsService userDetailsService() { daoAuthenticationProvider.setUserDetailsService(userDetailsService());
return new JdbcUserDetailsService(userRepository, userDetailsMapper); return daoAuthenticationProvider;
} }
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setUserDetailsPasswordService(userDetailsPasswordService());
daoAuthenticationProvider.setUserDetailsService(userDetailsService());
return daoAuthenticationProvider;
}
} }

View File

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

View File

@@ -6,9 +6,10 @@ import java.security.SecureRandom;
public class BCryptExample { public class BCryptExample {
public String encode(String plainPassword) { public String encode(String plainPassword) {
int strength = 10; int strength = 10;
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(strength, new SecureRandom()); BCryptPasswordEncoder bCryptPasswordEncoder =
return bCryptPasswordEncoder.encode(plainPassword); new BCryptPasswordEncoder(strength, new SecureRandom());
} return bCryptPasswordEncoder.encode(plainPassword);
}
} }

View File

@@ -4,13 +4,14 @@ import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
public class Pbkdf2Example { public class Pbkdf2Example {
public String encode(String plainPassword) { public String encode(String plainPassword) {
String pepper = "pepper"; // secret key used by password encoding String pepper = "pepper"; // secret key used by password encoding
int iterations = 200000; // number of hash iteration int iterations = 200000; // number of hash iteration
int hashWidth = 256; // hash with in bits int hashWidth = 256; // hash with in bits
Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder(pepper, iterations, hashWidth); Pbkdf2PasswordEncoder pbkdf2PasswordEncoder =
return pbkdf2PasswordEncoder.encode(plainPassword); new Pbkdf2PasswordEncoder(pepper, iterations, hashWidth);
} return pbkdf2PasswordEncoder.encode(plainPassword);
}
} }

View File

@@ -4,14 +4,15 @@ import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
public class SCryptExample { public class SCryptExample {
public String encode(String plainPassword) { public String encode(String plainPassword) {
int cpuCost = (int) Math.pow(2, 14); // factor to increase CPU costs int cpuCost = (int) Math.pow(2, 14); // factor to increase CPU costs
int memoryCost = 8; // factor to increases memory usage int memoryCost = 8; // factor to increases memory usage
int parallelization = 1; // currently nor supported by Spring Security int parallelization = 1; // currently nor supported by Spring Security
int keyLength = 32; // key length in bytes int keyLength = 32; // key length in bytes
int saltLength = 64; // salt length in bytes int saltLength = 64; // salt length in bytes
SCryptPasswordEncoder sCryptPasswordEncoder = new SCryptPasswordEncoder(cpuCost, memoryCost, parallelization, keyLength, saltLength); SCryptPasswordEncoder sCryptPasswordEncoder =
return sCryptPasswordEncoder.encode(plainPassword); new SCryptPasswordEncoder(cpuCost, memoryCost, parallelization, keyLength, saltLength);
} return sCryptPasswordEncoder.encode(plainPassword);
}
} }

View File

@@ -13,21 +13,19 @@ import org.springframework.stereotype.Component;
@Component @Component
public class PasswordMigration { public class PasswordMigration {
@Bean @Bean
public ApplicationListener<AuthenticationSuccessEvent> authenticationSuccessListener( public ApplicationListener<AuthenticationSuccessEvent> authenticationSuccessListener(
PasswordEncoder encoder, PasswordEncoder encoder, UserDetailsPasswordService userDetailsPasswordService) {
UserDetailsPasswordService userDetailsPasswordService) { return (AuthenticationSuccessEvent event) -> {
return (AuthenticationSuccessEvent event) -> { Authentication authentication = event.getAuthentication();
Authentication authentication = event.getAuthentication(); User user = (User) authentication.getPrincipal();
User user = (User) authentication.getPrincipal(); String encodedPassword = user.getPassword();
String encodedPassword = user.getPassword(); if (encodedPassword.startsWith("{SHA-1}")) {
if (encodedPassword.startsWith("{SHA-1}")) { CharSequence clearTextPassword = (CharSequence) authentication.getCredentials();
CharSequence clearTextPassword = (CharSequence) authentication.getCredentials(); String newPassword = encoder.encode(clearTextPassword);
String newPassword = encoder.encode(clearTextPassword); userDetailsPasswordService.updatePassword(user, newPassword);
userDetailsPasswordService.updatePassword(user, newPassword); }
} ((UsernamePasswordAuthenticationToken) authentication).eraseCredentials();
((UsernamePasswordAuthenticationToken) authentication).eraseCredentials(); };
}; }
}
} }

View File

@@ -11,6 +11,6 @@ import lombok.NoArgsConstructor;
@AllArgsConstructor @AllArgsConstructor
public class Car { public class Car {
private String name; private String name;
private String color; private String color;
} }

View File

@@ -8,19 +8,11 @@ import java.util.Set;
@RestController @RestController
public class CarResources { public class CarResources {
// we use this endpoint as authentication test // we use this endpoint as authentication test
@GetMapping("/cars") @GetMapping("/cars")
public Set<Car> cars() { public Set<Car> cars() {
return Set.of( return Set.of(
Car.builder() Car.builder().name("vw").color("black").build(),
.name("vw") Car.builder().name("bmw").color("white").build());
.color("black") }
.build(),
Car.builder()
.name("bmw")
.color("white")
.build()
);
}
} }

View File

@@ -11,6 +11,6 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor @NoArgsConstructor
public class UserCredentialsDto { public class UserCredentialsDto {
private String username; private String username;
private String password; private String password;
} }

View File

@@ -16,24 +16,24 @@ import java.util.Set;
@Transactional @Transactional
public class UserResources { public class UserResources {
private final UserRepository userRepository; private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
public UserResources(UserRepository userRepository, PasswordEncoder passwordEncoder) { public UserResources(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository; this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
} }
@PostMapping("/registration")
@PostMapping("/registration") @ResponseStatus(code = HttpStatus.CREATED)
@ResponseStatus(code = HttpStatus.CREATED) public void register(@RequestBody UserCredentialsDto userCredentialsDto) {
public void register(@RequestBody UserCredentialsDto userCredentialsDto) { UserCredentials user =
UserCredentials user = UserCredentials.builder() UserCredentials.builder()
.enabled(true) .enabled(true)
.username(userCredentialsDto.getUsername()) .username(userCredentialsDto.getUsername())
.password(passwordEncoder.encode(userCredentialsDto.getPassword())) .password(passwordEncoder.encode(userCredentialsDto.getPassword()))
.roles(Set.of("USER")) .roles(Set.of("USER"))
.build(); .build();
userRepository.save(user); userRepository.save(user);
} }
} }

View File

@@ -9,84 +9,93 @@ import java.util.concurrent.TimeUnit;
@Component @Component
public class BcCryptWorkFactorService { public class BcCryptWorkFactorService {
private static final String TEST_PASSWORD = "my password"; private static final String TEST_PASSWORD = "my password";
private static final int GOAL_MILLISECONDS_PER_PASSWORD = 1000; private static final int GOAL_MILLISECONDS_PER_PASSWORD = 1000;
private static final int MIN_STRENGTH = 4; private static final int MIN_STRENGTH = 4;
private static final int MAX_STRENGTH = 31; 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(
* Calculates the strength (a.k.a. log rounds) for the BCrypt Algorithm, so that password encoding takes about 1s. BcryptWorkFactor smallFactor, BcryptWorkFactor bigFactor) {
* This method iterates over strength from 4 to 31 and calculates the duration of password encoding for every value of strength. if (bigFactor.getStrength() - smallFactor.getStrength() == 1) {
* It returns the first strength, that takes more than 500s return getClosestStrength(smallFactor, bigFactor);
*/
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);
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));
} }
int midStrength =
/** (bigFactor.getStrength() - smallFactor.getStrength()) / 2 + smallFactor.getStrength();
* Calculates the strength (a.k.a. log rounds) for the BCrypt Algorithm, so that password encoding takes about 1s. long duration = calculateDuration(midStrength);
* This method iterate over strength from 4 to 31 and calculates the duration of password encoding for every value of strength. BcryptWorkFactor midFactor = new BcryptWorkFactor(midStrength, duration);
* When the the duration takes more than 1s, it is compared to previous one and the method returns the strength, tha is closer if (duration < GOAL_MILLISECONDS_PER_PASSWORD) {
* to 1s. return calculateStrengthDivideAndConquer(midFactor, bigFactor);
*/
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));
} }
return calculateStrengthDivideAndConquer(smallFactor, midFactor);
}
/** private BcryptWorkFactor getClosestStrength(
* @param previousDuration duration from previous iteration BcryptWorkFactor smallFactor, BcryptWorkFactor bigFactor) {
* @param currentDuration duration of current iteration if (isPreviousDurationCloserToGoal(smallFactor.getDuration(), bigFactor.getDuration())) {
* @param strength current strength return smallFactor;
* @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)) {
return strength - 1;
} else {
return strength;
}
} }
return bigFactor;
}
private boolean isGreaterThanGoal(long duration) { private long calculateDuration(int strength) {
return duration > GOAL_MILLISECONDS_PER_PASSWORD; BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(strength);
} Stopwatch stopwatch = Stopwatch.createStarted();
bCryptPasswordEncoder.encode(TEST_PASSWORD);
stopwatch.stop();
return stopwatch.elapsed(TimeUnit.MILLISECONDS);
}
/** /**
* return true, if previousDuration is closer to the goal than currentDuration, false otherwise. * 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
boolean isPreviousDurationCloserToGoal(long previousDuration, long currentDuration) { * password encoding for every value of strength. It returns the first strength, that takes more
return Math.abs(GOAL_MILLISECONDS_PER_PASSWORD - previousDuration) < Math.abs(GOAL_MILLISECONDS_PER_PASSWORD - currentDuration); * than 1s
*/
public int calculateStrength() {
for (int strength = MIN_STRENGTH; strength <= MAX_STRENGTH; strength++) {
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));
}
/**
* @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.
*/
int getStrength(long previousDuration, long currentDuration, int strength) {
if (isPreviousDurationCloserToGoal(previousDuration, currentDuration)) {
return strength - 1;
} else {
return strength;
}
}
/**
* 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);
}
} }

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

@@ -9,31 +9,33 @@ import java.util.concurrent.TimeUnit;
@Component @Component
public class Pbkdf2WorkFactorService { public class Pbkdf2WorkFactorService {
private static final String TEST_PASSWORD = "my password"; private static final String TEST_PASSWORD = "my password";
private static final String NO_ADDITIONAL_SECRET = ""; private static final String NO_ADDITIONAL_SECRET = "";
private static final int GOAL_MILLISECONDS_PER_PASSWORD = 1000; private static final int GOAL_MILLISECONDS_PER_PASSWORD = 1000;
private static final int HASH_WIDTH = 256; private static final int HASH_WIDTH = 256;
private static final int ITERATION_STEP = 5000; private static final int ITERATION_STEP = 5000;
/** /**
* Finds the number of Iteration for the {@link Pbkdf2PasswordEncoder} to get the duration of password encoding * Finds the number of Iteration for the {@link Pbkdf2PasswordEncoder} to get the duration of
* close to 1s. The Calculation does not use any secret (pepper) and applies hash algorithm SHA256. * password encoding close to 1s. The Calculation does not use any secret (pepper) and applies
*/ * hash algorithm SHA256.
public int calculateIteration() { */
public int calculateIteration() {
int iterationNumber = 150000; int iterationNumber = 150000;
while (true) { 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(); Stopwatch stopwatch = Stopwatch.createStarted();
pbkdf2PasswordEncoder.encode(TEST_PASSWORD); pbkdf2PasswordEncoder.encode(TEST_PASSWORD);
stopwatch.stop(); stopwatch.stop();
long duration = stopwatch.elapsed(TimeUnit.MILLISECONDS); long duration = stopwatch.elapsed(TimeUnit.MILLISECONDS);
if (duration > GOAL_MILLISECONDS_PER_PASSWORD) { if (duration > GOAL_MILLISECONDS_PER_PASSWORD) {
return iterationNumber; return iterationNumber;
} }
iterationNumber += ITERATION_STEP; iterationNumber += ITERATION_STEP;
}
} }
}
} }

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)
insert into users (username, password, enabled) VALUES ('user', '{SHA-256}{4Cc0+yDMHnTUy+zOHeMH7yaPhxvlJT//tQTwEhyegiQ=}446d06130bfc254527a7bbd95b50595a977c0058110f8dccb54bd273d99325b8', true); VALUES ('admin', '{bcrypt}$2a$10$4V9kA793Pi2xf94dYFgKWuw8ukyETxWb7tZ4/mfco9sWkwvBQndxW', 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)
insert into users (username, password, enabled) VALUES ('user with sha1 encoding', '{SHA-1}{6tND0AZfFH3aE1VDg7QkWT6DzFg/NUHtukntgwu8JV4=}804c6e8efebf4e91f88e3baf9fd383e28a21378c', true); VALUES ('user',
insert into users (username, password, enabled) VALUES ('scrypt user', '{scrypt}$e0801$fUx3MxN07zdH3UyARJqOwv3WiWCvE7f6qRm9A5KQfNo5ovSwxMHknQ4vERO4csj/I3imG2HJQg1HHp7Rqzbp7g==$Fm5F9PSoE/jBYLOmnCJcvX1Euf952r5b3BjAl+SwQMs=', true); '{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

@@ -9,24 +9,25 @@ import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
class UserDetailsMapperTest { class UserDetailsMapperTest {
private UserDetailsMapper userDetailsMapper = new UserDetailsMapper(); private UserDetailsMapper userDetailsMapper = new UserDetailsMapper();
@Test @Test
void toUserDetails() { void toUserDetails() {
// given // given
UserCredentials userCredentials = UserCredentials.builder() UserCredentials userCredentials =
.enabled(true) UserCredentials.builder()
.password("password") .enabled(true)
.username("user") .password("password")
.roles(Set.of("USER", "ADMIN")) .username("user")
.build(); .roles(Set.of("USER", "ADMIN"))
.build();
// when // when
UserDetails userDetails = userDetailsMapper.toUserDetails(userCredentials); UserDetails userDetails = userDetailsMapper.toUserDetails(userCredentials);
// then // then
assertThat(userDetails.getUsername()).isEqualTo("user"); assertThat(userDetails.getUsername()).isEqualTo("user");
assertThat(userDetails.getPassword()).isEqualTo("password"); assertThat(userDetails.getPassword()).isEqualTo("password");
assertThat(userDetails.isEnabled()).isTrue(); assertThat(userDetails.isEnabled()).isTrue();
} }
} }

View File

@@ -9,18 +9,17 @@ import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
@DataJpaTest @DataJpaTest
class UserRepositoryTest { class UserRepositoryTest {
@Autowired @Autowired private UserRepository userRepository;
private UserRepository userRepository;
@Test @Test
void findUserByUsername() { void findUserByUsername() {
// given // given
String username = "user"; String username = "user";
// when // when
UserCredentials userCredentials = userRepository.findByUsername(username); UserCredentials userCredentials = userRepository.findByUsername(username);
// then // then
assertThat(userCredentials).isNotNull(); assertThat(userCredentials).isNotNull();
} }
} }

View File

@@ -6,17 +6,17 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
class Argon2ExampleTest { class Argon2ExampleTest {
private Argon2Example argon2Example = new Argon2Example(); private Argon2Example argon2Example = new Argon2Example();
@Test @Test
void encode() { void encode() {
// given // given
String plainPassword = "password"; String plainPassword = "password";
// when // when
String actual = argon2Example.encode(plainPassword); String actual = argon2Example.encode(plainPassword);
// then // then
assertThat(actual).startsWith("$argon2id$v=19$m=4096,t=3,p=1"); assertThat(actual).startsWith("$argon2id$v=19$m=4096,t=3,p=1");
} }
} }

View File

@@ -6,17 +6,17 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
class BCryptExampleTest { class BCryptExampleTest {
private BCryptExample bcryptExample = new BCryptExample(); private BCryptExample bcryptExample = new BCryptExample();
@Test @Test
void encode() { void encode() {
// given // given
String plainPassword = "password"; String plainPassword = "password";
// when // when
String encoded = bcryptExample.encode(plainPassword); String encoded = bcryptExample.encode(plainPassword);
// then // then
assertThat(encoded).startsWith("$2a$10"); assertThat(encoded).startsWith("$2a$10");
} }
} }

View File

@@ -6,17 +6,17 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
class Pbkdf2ExampleTest { class Pbkdf2ExampleTest {
private Pbkdf2Example pbkdf2Example = new Pbkdf2Example(); private Pbkdf2Example pbkdf2Example = new Pbkdf2Example();
@Test @Test
void encode() { void encode() {
// given // given
String plainPassword = "plainPassword"; String plainPassword = "plainPassword";
// when // when
String actual = pbkdf2Example.encode(plainPassword); String actual = pbkdf2Example.encode(plainPassword);
// then // then
assertThat(actual).hasSize(80); assertThat(actual).hasSize(80);
} }
} }

View File

@@ -6,18 +6,18 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
class SCryptExampleTest { class SCryptExampleTest {
private SCryptExample sCryptExample = new SCryptExample(); private SCryptExample sCryptExample = new SCryptExample();
@Test @Test
void encode() { void encode() {
// given // given
String plainPassword = "password"; String plainPassword = "password";
// when // when
String actual = sCryptExample.encode(plainPassword); String actual = sCryptExample.encode(plainPassword);
// then // then
assertThat(actual).hasSize(140); assertThat(actual).hasSize(140);
assertThat(actual).startsWith("$e0801"); assertThat(actual).startsWith("$e0801");
} }
} }

View File

@@ -22,73 +22,70 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@Transactional @Transactional
class CarResourcesTest { class CarResourcesTest {
@Autowired @Autowired private MockMvc mockMvc;
private MockMvc mockMvc;
@Autowired @Autowired private ObjectMapper objectMapper;
private ObjectMapper objectMapper;
@Autowired @Autowired private UserRepository userRepository;
private UserRepository userRepository;
@Test @Test
void getCarsShouldReturnUnauthorizedIfTheRequestHasNoBasicAuthentication() throws Exception { void getCarsShouldReturnUnauthorizedIfTheRequestHasNoBasicAuthentication() throws Exception {
mockMvc.perform(get("/cars")) mockMvc.perform(get("/cars")).andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized()); }
}
@Test @Test
void getCarsShouldReturnCarsForTheAuthenticatedUser() throws Exception { void getCarsShouldReturnCarsForTheAuthenticatedUser() throws Exception {
mockMvc.perform(get("/cars") mockMvc.perform(get("/cars").with(httpBasic("user", "password"))).andExpect(status().isOk());
.with(httpBasic("user", "password"))) }
.andExpect(status().isOk());
}
@Test @Test
void registrationShouldReturnCreated() throws Exception { void registrationShouldReturnCreated() throws Exception {
// register // register
UserCredentialsDto userCredentialsDto = UserCredentialsDto.builder().username("toyota").password("my secret").build(); UserCredentialsDto userCredentialsDto =
mockMvc.perform(post("/registration") UserCredentialsDto.builder().username("toyota").password("my secret").build();
mockMvc
.perform(
post("/registration")
.contentType(MediaType.APPLICATION_JSON_VALUE) .contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(userCredentialsDto))) .content(objectMapper.writeValueAsString(userCredentialsDto)))
.andExpect(status().isCreated()); .andExpect(status().isCreated());
} }
@Test @Test
void registrationShouldReturnUnauthorizedWithWrongCredentials() throws Exception { void registrationShouldReturnUnauthorizedWithWrongCredentials() throws Exception {
mockMvc.perform(get("/cars") mockMvc
.with(httpBasic("user", "wrong password"))) .perform(get("/cars").with(httpBasic("user", "wrong password")))
.andExpect(status().isUnauthorized()); .andExpect(status().isUnauthorized());
} }
@Test @Test
void getCarsShouldUpdatePasswordFromWorkingFactor5toHigherValue() throws Exception { void getCarsShouldUpdatePasswordFromWorkingFactor5toHigherValue() throws Exception {
mockMvc.perform(get("/cars") mockMvc
.with(httpBasic("user with working factor 5", "password"))) .perform(get("/cars").with(httpBasic("user with working factor 5", "password")))
.andExpect(status().isOk()); .andExpect(status().isOk());
UserCredentials userCredentials = userRepository.findByUsername("user with working factor 5"); UserCredentials userCredentials = userRepository.findByUsername("user with working factor 5");
// we don't know what strength the BcCryptWorkFactorService returns, // we don't know what strength the BcCryptWorkFactorService returns,
// but it should be more than 5 // but it should be more than 5
assertThat(userCredentials.getPassword()).doesNotStartWith("{bcrypt}$2a$05"); assertThat(userCredentials.getPassword()).doesNotStartWith("{bcrypt}$2a$05");
} }
@Test @Test
void getCarsShouldUpdateSha1PasswordToBcrypt() throws Exception { void getCarsShouldUpdateSha1PasswordToBcrypt() throws Exception {
mockMvc.perform(get("/cars") mockMvc
.with(httpBasic("user with sha1 encoding", "password"))) .perform(get("/cars").with(httpBasic("user with sha1 encoding", "password")))
.andExpect(status().isOk()); .andExpect(status().isOk());
UserCredentials userCredentials = userRepository.findByUsername("user with sha1 encoding"); UserCredentials userCredentials = userRepository.findByUsername("user with sha1 encoding");
assertThat(userCredentials.getPassword()).startsWith("{bcrypt}"); assertThat(userCredentials.getPassword()).startsWith("{bcrypt}");
} }
@Test @Test
void getCarsShouldReturnOkForScryptUser() throws Exception { void getCarsShouldReturnOkForScryptUser() throws Exception {
mockMvc.perform(get("/cars") mockMvc
.with(httpBasic("scrypt user", "password"))) .perform(get("/cars").with(httpBasic("scrypt user", "password")))
.andExpect(status().isOk()); .andExpect(status().isOk());
} }
} }

View File

@@ -1,121 +1,119 @@
package io.reflectoring.passwordencoding.workfactor; package io.reflectoring.passwordencoding.workfactor;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
class BcCryptWorkFactorServiceTest { class BcCryptWorkFactorServiceTest {
private BcCryptWorkFactorService bcCryptWorkFactorService = new BcCryptWorkFactorService(); private BcCryptWorkFactorService bcCryptWorkFactorService = new BcCryptWorkFactorService();
@Test @Test
void calculateStrength() { void calculateStrength() {
// given // given
// when // when
int strength = bcCryptWorkFactorService.calculateStrength(); int strength = bcCryptWorkFactorService.calculateStrength();
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(strength); // then
assertThat(strength).isBetween(4, 31);
}
// then @Test
assertThat(strength).isBetween(4, 31); void calculateStrengthBi() {
} // given
@Test // when
void calculateRounds() { BcryptWorkFactor bcryptWorkFactor =
// given bcCryptWorkFactorService.calculateStrengthDivideAndConquer();
// when // then
int strength = bcCryptWorkFactorService.calculateStrengthClosestToTimeGoal(); assertThat(bcryptWorkFactor.getStrength()).isBetween(4, 31);
}
// then @Test
assertThat(strength).isBetween(4, 31); void findCloserToShouldReturnNumber1IfItCloserToGoalThanNumber2() {
} // given
int number1 = 950;
int number2 = 1051;
@Test // when
void findCloserToShouldReturnNumber1IfItCloserToGoalThanNumber2() { boolean actual = bcCryptWorkFactorService.isPreviousDurationCloserToGoal(number1, number2);
// given
int number1 = 950;
int number2 = 1051;
// when // then
boolean actual = bcCryptWorkFactorService.isPreviousDurationCloserToGoal(number1, number2); assertThat(actual).isTrue();
}
// then @Test
assertThat(actual).isTrue(); void findCloserToShouldReturnNUmber2IfItCloserToGoalThanNumber1() {
} // given
int number1 = 1002;
int number2 = 999;
@Test // when
void findCloserToShouldReturnNUmber2IfItCloserToGoalThanNumber1() { boolean actual = bcCryptWorkFactorService.isPreviousDurationCloserToGoal(number1, number2);
// given
int number1 = 1002;
int number2 = 999;
// when // then
boolean actual = bcCryptWorkFactorService.isPreviousDurationCloserToGoal(number1, number2); assertThat(actual).isFalse();
}
// then @Test
assertThat(actual).isFalse(); void findCloserToShouldReturnGoalIfNumber2IsEqualGoal() {
} // given
int number1 = 999;
int number2 = 1000;
@Test // when
void findCloserToShouldReturnGoalIfNumber2IsEqualGoal() { boolean actual = bcCryptWorkFactorService.isPreviousDurationCloserToGoal(number1, number2);
// given
int number1 = 999;
int number2 = 1000;
// when // then
boolean actual = bcCryptWorkFactorService.isPreviousDurationCloserToGoal(number1, number2); assertThat(actual).isFalse();
}
// then @Test
assertThat(actual).isFalse(); void findCloserToShouldReturnGoalIfNumber1IsEqualGoal() {
} // given
int number1 = 1000;
int number2 = 1001;
@Test // when
void findCloserToShouldReturnGoalIfNumber1IsEqualGoal() { boolean actual = bcCryptWorkFactorService.isPreviousDurationCloserToGoal(number1, number2);
// given
int number1 = 1000;
int number2 = 1001;
// when // then
boolean actual = bcCryptWorkFactorService.isPreviousDurationCloserToGoal(number1, number2); assertThat(actual).isTrue();
}
// then @Test
assertThat(actual).isTrue(); void getStrengthShouldReturn4IfStrengthIs4() {
} // given
int currentStrength = 4;
@Test // when
void getStrengthShouldReturn4IfStrengthIs4() { int actual = bcCryptWorkFactorService.getStrength(0, 0, currentStrength);
// given
int currentStrength = 4;
// when // then
int actual = bcCryptWorkFactorService.getStrength(0, 0, currentStrength); assertThat(actual).isEqualTo(4);
}
// then @Test
assertThat(actual).isEqualTo(4); void getStrengthShouldReturnPreviousStrengthIfPreviousDurationCloserToGoal() {
} // given
@Test // when
void getStrengthShouldReturnPreviousStrengthIfPreviousDurationCloserToGoal() { int actual = bcCryptWorkFactorService.getStrength(980, 1021, 5);
// given
// when // then
int actual = bcCryptWorkFactorService.getStrength(980, 1021, 5); assertThat(actual).isEqualTo(4);
}
// then @Test
assertThat(actual).isEqualTo(4); void getStrengthShouldReturnCurrentStrengthIfCurrentDurationCloserToGoal() {
} // given
@Test // when
void getStrengthShouldReturnCurrentStrengthIfCurrentDurationCloserToGoal() { int actual = bcCryptWorkFactorService.getStrength(960, 1021, 5);
// given
// when // then
int actual = bcCryptWorkFactorService.getStrength(960, 1021, 5); assertThat(actual).isEqualTo(5);
}
// then }
assertThat(actual).isEqualTo(5);
}
}

View File

@@ -6,17 +6,16 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
class Pbkdf2WorkFactorServiceTest { class Pbkdf2WorkFactorServiceTest {
private Pbkdf2WorkFactorService pbkdf2WorkFactorService = new Pbkdf2WorkFactorService();
private Pbkdf2WorkFactorService pbkdf2WorkFactorService = new Pbkdf2WorkFactorService(); @Test
void calculateIteration() {
// given
@Test // when
void calculateIteration() { int iterationNumber = pbkdf2WorkFactorService.calculateIteration();
// given
// when // then
int iterationNumber = pbkdf2WorkFactorService.calculateIteration(); assertThat(iterationNumber).isGreaterThanOrEqualTo(150000);
}
// then }
assertThat(iterationNumber).isGreaterThanOrEqualTo(150000);
}
}