비밀번호 암호화 적용하기(Spring Security Crypto)
https://20240228.tistory.com/153
비밀번호 암호화
목차암호화 종류단방향 암호화 방법단방향 암호화 문제점 개선 (+ Salt)단방향 암호화 문제점 개선 (+ Key Stretching)양방향 암호화 특징 참고 자료암호화 종류 암호화를 하는 이유는 해당
20240228.tistory.com
최근에 비밀번호 암호화에 대해서 공부했었다.
그중에 단방향 암호화 라이브러리인 Spring Security Crypto를 사용해서 프로젝트에 적용해 보겠다.
라이브러리 Dependency 찾기
Maven Repository : Spring Security Crypto 검색
build.gradle
특정 버전 명시 O
dependencies {
implementation group: 'org.springframework.security', name: 'spring-security-crypto', version: '6.3.3'
}
특정 버전 명시 X
dependencies {
implementation 'org.springframework.security:spring-security-crypto'
}
회원가입 서비스에 적용하기
public void signUp(SignUp signup) {
Optional<User> findDuplicateUser = userRepository.findByEmail(signup.getEmail());
if(findDuplicateUser.isPresent()){
throw new AlreadyExistsEmailException();
}
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder(
16,
8,
1,
32,
64);
String encryptedPassword = encoder.encode(signup.getPassword());
log.info("encryptedPassword:{}", encryptedPassword);
User user = User.builder()
.name(signup.getName())
.password(encryptedPassword)
.email(signup.getEmail())
.build();
userRepository.save(user);
}
내가 적용한 암호화 부분
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder(
16, // cpuCost
8, // memoryCost
1, // parallelization
32, // keyLength
64);// saltLength
String encryptedPassword = encoder.encode(signup.getPassword());
log.info("encryptedPassword:{}", encryptedPassword);
SCryptPasswordEncoder 분석
SCryptPasswordEncoder (spring-security-docs 6.3.3 API)
Constructs a SCrypt password encoder with cpu cost of 65,536, memory cost of 8, parallelization of 1, a key length of 32 and a salt length of 16 bytes.
docs.spring.io
SCryptPasswordEncoder 생성
SHA-1 이상 해시와 8바이트 이상 랜덤 생성 솔트를 결합
당연히 솔트 부분은 내가 랜덤하게 넣어줘야 할 줄 알았는데 알아서 랜덤으로 넣어주는 게 신기하다.
이러면 어떻게 나중에 DB에 저장된(암호화된) 비밀번호와 입력한 비밀번호가 동일한지 비교를 어떻게 하는지 궁금하다.
SCryptPasswordEncoder
private final BytesKeyGenerator saltGenerator;
public SCryptPasswordEncoder(int cpuCost, int memoryCost, int parallelization, int keyLength, int saltLength) {
// 생략 ...
this.saltGenerator = KeyGenerators.secureRandom(saltLength);
}
SCryptPasswordEncoder 생성자이다.
해당 코드를 보면 생성자에서 객체 생성시점에 BytesKeyGenerator 값을 할당을 하는데
이때 KeyGenerators.secureRandom(saltLength);를 통해서 할당한다.
KeyGenerators
public static BytesKeyGenerator secureRandom(int keyLength) {
return new SecureRandomBytesKeyGenerator(keyLength);
}
여기서 다시 SecureRandomBytesKeyGenerator(keyLength); (생성자 호출)
SecureRandomBytesKeyGenerator
final class SecureRandomBytesKeyGenerator implements BytesKeyGenerator {
private static final int DEFAULT_KEY_LENGTH = 8;
private final SecureRandom random;
private final int keyLength;
SecureRandomBytesKeyGenerator(int keyLength) {
this.random = new SecureRandom();
this.keyLength = keyLength;
}
// 생략 ...
}
SecureRandomBytesKeyGenerator 클래스에 SecureRandom 생성하고 salt 길이 또한 저장을 한다.
SecureRandom은 랜덤한 바이트 salt 생성에 사용
SecureRandomBytesKeyGenerator
@Override
public int getKeyLength() {
return this.keyLength;
}
@Override
public byte[] generateKey() {
byte[] bytes = new byte[this.keyLength];
this.random.nextBytes(bytes);
return bytes;
}
이부분을 암호화할 때 사용한다.
this.random.nextBytes(bytes) 이부분은 랜덤한 salt 생성
org.bouncycastle.crypto.generators.SCrypt
브레이크 포인트를 찍고 디버깅 모드로 분석하던중 예외가 발생했다.
java.lang.ClassNotFoundException: org.bouncycastle.crypto.generators.SCrypt
build.gradle 추가로 해결
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
SCryptPasswordEncoder.matches() - 비교 메서드
SCryptPasswordEncoder.matches()
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() < this.keyLength) {
this.logger.warn("Empty encoded password");
return false;
}
return decodeAndCheckMatches(rawPassword, encodedPassword);
}
SCryptPasswordEncoder.decodeAndCheckMatches()
private boolean decodeAndCheckMatches(CharSequence rawPassword, String encodedPassword) {
String[] parts = encodedPassword.split("\\$");
if (parts.length != 4) {
return false;
}
long params = Long.parseLong(parts[1], 16);
byte[] salt = decodePart(parts[2]);
byte[] derived = decodePart(parts[3]);
int cpuCost = (int) Math.pow(2, params >> 16 & 0xffff);
int memoryCost = (int) params >> 8 & 0xff;
int parallelization = (int) params & 0xff;
byte[] generated = SCrypt.generate(Utf8.encode(rawPassword), salt, cpuCost, memoryCost, parallelization,
this.keyLength);
return MessageDigest.isEqual(derived, generated);
}
암호화되지 않은 비밀번호와 암호화된 비밀번호를 메서드를 매개변수로 받고
내부에서 암호화되지 않은 비밀번호를 암호화했던 비밀번호와 동일한 방식(해시 알고리즘, 키 길이, salt, ...)으로
암호화해서 같은지 비교하고 같으면 true, 다르면 false를 반환한다.