PasswordEncoder
Spring Security에서는 비밀번호를 안전하게 저장할 수 있도록 비밀번호의 단방향 암호화를 지원하는 PasswordEncoder 인터페이스와 구현체들을 제공합니다. 이 인터페이스는 아래와 같이 심플하게 구성되어 있습니다.
public interface PasswordEncoder {
// 비밀번호를 단방향 암호화
String encode(CharSequence rawPassword);
// 암호화되지 않은 비밀번호(raw-)와 암호화된 비밀번호(encoded-)가 일치하는지 비교
boolean matches(CharSequence rawPassword, String encodedPassword);
// 암호화된 비밀번호를 다시 암호화하고자 할 경우 true를 return하게 설정
default boolean upgradeEncoding(String encodedPassword) { return false; };
}
Spring Securit PasswordEncoder 구현 클래스들은 아래와 같습니다.
- BcryptPasswordEncoder : BCrypt 해시 함수를 사용해 비밀번호를 암호화
- Argon2PasswordEncoder : Argon2 해시 함수를 사용해 비밀번호를 암호화
- Pbkdf2PasswordEncoder : PBKDF2 해시 함수를 사용해 비밀번호를 암호화
- SCryptPasswordEncoder : SCrypt 해시 함수를 사용해 비밀번호를 암호화
StandardPasswordEncoder : SHA-256을 이용해 암호를 해시한다 이구현은 이제는 구식이며 새구현에는 쓰지말아야함
NoOpPasswordEncoder의 가장 단순한 구현형태
@Deprecated
public final class NoOpPasswordEncoder implements PasswordEncoder {
private static final PasswordEncoder INSTANCE = new NoOpPasswordEncoder();
public String encode(CharSequence rawPassword) {
return rawPassword.toString(); // <- 암호를 변경하지 않고 그대로 반환
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.toString().equals(encodedPassword); //두문자열이 같은지 확인
}
public static PasswordEncoder getInstance() {
return INSTANCE;
}
private NoOpPasswordEncoder() {
}
}
NoOpPasswwordEncoder - 암호를 인코딩하지 않고 일반 텍스트로 유지함,. 이구현은 예제로 보여주기로만 적당하며 암호를 해시하지 않기 때문에 실제로 절대 쓰지말아야함
SHA-512를 이용하는 PasswordEncoder 구현
public class Sha512PasswordEncoder implements PasswordEncoder {
@Override
public boolean upgradeEncoding(String encodedPassword) {
return PasswordEncoder.super.upgradeEncoding(encodedPassword);
}
@Override
public String encode(CharSequence charSequence) {
return hasWithSHA512(charSequence.toString());
}
@Override
public boolean matches(CharSequence charSequence, String s) {
String hashedPassword = encode(charSequence);
return s.equals(hashedPassword);
}
private String hasWithSHA512(String input){
StringBuilder result = new StringBuilder();
try{
MessageDigest md = MessageDigest.getInstance("SHA-512");
byte[] digested = md.digest(input.getBytes());
for(int i=0;i<digested.length;i++){
result.append(Integer.toHexString(0xFF &digested[i]));
}
}catch (NoSuchAlgorithmException e){
throw new RuntimeException("Bad algorithm");
}
return result.toString();
}
}
1.1. Hash 함수?
해시 함수(= 해시 알고리즘)
메시지 인증과 무결성 체크를 위해 이용됩니다.
단방향 암호 알고리즘이기 때문에 해시값을 복호화 할 수 없습니다.
원본 데이터의 내용이 같을 경우 동일한 해시값을 리턴하는 성질을 이용하여 데이터 무결성을 확인합니다.
단독으로 사용할 경우 처리 속도가 매우 빠릅니다.
매우 빠른 처리 속도는 공격자들의 무단 공격에 매우 취약한 단점이 됩니다. 이러한 이유로 비밀번호와 같이 강력한 보안이 필요한 부분에는 Key Derivation Function을 사용하는 것을 권장합니다.
해시 함수를 통해 암호화된 Hash값은 digest, Checksums, digital fingerprints 라고도 부릅니다.
원본 데이터(message)를 해시 알고리즘으로 암호화한 Hash 값을 digest라고 부릅니다.
@SneakyThrows
static String encrypt(String message) {
final MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(message.getBytes());
return new String(Base64.getEncoder().encode(messageDigest.digest()));
}
- MD2
- RFC 1319에 의해 정의된 MD2 Message Digest 알고리즘
- MD5
- RFC 1321에 의해 정의된 MD5 Message Digest 알고리즘
- SHA-1, SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224, SHA-512/256
- Secure Hash Algorithm
- FIPS PUB 180-4 에 의해 정의된 보안 해시 알고리즘
- SHA-1는 160bit digest를 생성합니다
- SHA-224(224 bit), SHA-256(256 bit), SHA-384(384 bit), SHA-512(512 bit), SHA-512/224(224 bit), SHA-512/256(256 bit)
- SHA3-224, SHA3-256, SHA3-384, SHA3-512
- FIPS PUB 202에 정의된 확장가능한 출력기능을 갖는 순열 기반 해시 함수
아래는 "hello jiniworld" 문자열을 SHA-256으로 암호화한 결과입니다.
입력값이 같을 경우 동일한 해시를 만들기 때문에, "hello jiniworld"의 digest는 언제나 아래와 같습니다.
fBWgim+2DAp0+01x5Ylpr9pPFC7zbx5GMmofaw5+sGI=
여기에 느낌표만 하나 붙여서 "hello jiniworld!" 을 암호화해보니 아래와 같은 결과값이 리턴되었습니다.
"hello jiniworld" 의 해시값과 결과가 전혀 다른 것을 확인할 수 있습니다.
A7Z8clE8u2kBxXxYgArInfN49/VKiOjFXw5zXXvW74g=
이러한 특성을 Avalanche effect(산사태 효과)라고 말합니다.
입력값에 미세한 변화가 출력값에 상당한 변화를 주는 특성을 의미하고, 이 특성이 강할 수록 강력한 알고리즘임으로 의미합니다.
rainbow attack
다만 해시 함수는 message 입력값이 같으면 암호화된 Digest가 동일하기 때문에,
digest값이 대량으로 탈취되었을 때 공격자가 Rainbow Table을 이용하여 원본 message를 추출할 수 있다는 취약점이 존재합니다.
빠른 변환 속도
해시 함수가 최초로 설계된 목적이 빠르게 데이터를 검색하기 위한 것인 만큼. 기본적으로 해시함수는 처리속도가 매우 빠릅니다.
MD5 알고리즘의 경우 일반적인 컴퓨터를 이용한다 하더라고 1초에 56억개 가략의 digest 추론이 가능하다고 합니다.
여기에다 원문의 길이가 짧고 단순할 경우 암호 원문 추론은 더 간단해집니다.
변환이 매우 빠른 부분은 어쩌면 공격하는 이들에게 더 유리한 부분이 있습니다.
2.1. salt
단방향 해시 알고리즘에서 digest를 생성할 때 이용하는 byte단위 문자열로, 외부에서 rainbow attack을 방지하기 위해 이용합니다.
해시 알고리즘 뿐만 아니라 추론하기 어려운 임의의 salt 문자열을 이용하기 때문에 digest가 탈취되어도 salt를 모르면 원문을 추론하기 어렵습니다.
보안을 강화하기 위해 암호화하고자 하는 원문 각각당 랜덤으로 생성한 salt를 갖고, 길이를 적정 수준으로 설정하는 것을 권장합니다.
2.2. key stretching
입력한 패스워드에 대한 digest를 생성하고, 그 digest를 입력값으로 이용하여 다시 digest로 만드는 행위를 N번 반복합니다.
반복하는 iteration count를 동일한 횟수만큼 해싱해야 비밀번호 일치여부를 확인할 수 있습니다.
brute-force attack(무차별 대입 공격)을 이용하여 password 추측을 쉽게 할 수 있는 것을 방지하기 위해 키 스트레칭을 이용하여 하나의 digest의 생성 시간을 0.2초 이상으로 설정하는 것을 권장합니다.
키 스트레칭은 보통 랜덤으로 생성한 salt값과 함께 이용합니다.
password와 salt를 이용하여 iteration count 만큼 해싱을 반복하여 최종 digest를 얻습니다.
Blowfish 블록 암호화 알고리즘을 해시함수로 변형한 것으로, 비밀번호 저장을 목적으로 설계되었습니다.
Bcrypt 알고리즘은 JCA에서 기본적으로 제공되지는 않고, Spring Security 라이브러리에서 제공하고 있어, 라이브러리를 추가하면 매우 간편히 사용할 수 있습니다.
bcrypt 알고리즘의 digest는 bcrypt 알고리즘 버전, 키스트레칭 횟수, salt, hash 를 합친형태로,
암호화할때마다 salt값이 바뀌기 때문에 암호화할때마다 hash및 전체 digest값이 바뀝니다.
encode 된 결과끼리는 서로 일치하는지 같은 문자열을 암호화한 결과끼리는 매칭 검사 할 수 없고
원문과 암호화된 결과의 매칭 확인은 가능합니다.
dependencies {
implementation 'org.springframework.security:spring-security-core:5.7.2'
}
아래 코드는 BCryptPasswordEncoder을 이용하여 간단하게 BCrypt 암호화를 테스트 해보았습니다.
참고로 BCryptPasswordEncoder 생성자에 파라미터를 설정하지 않으면 기본적으로 Bcrypt 알고리즘 버전은 $2A, 키스트레칭 횟수는 10입니다.
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class BcryptExample {
public static final BCryptPasswordEncoder bCryptPasswordEncoder =
new BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion.$2B, 12);
public static void main(String[] args) {
String rawPassword = "password2213@";
String encPassword = bCryptPasswordEncoder.encode(rawPassword);
String encPassword2 = bCryptPasswordEncoder.encode(rawPassword);
System.out.println(encPassword);
System.out.println(encPassword2);
System.out.println(bCryptPasswordEncoder.matches(rawPassword, encPassword));
System.out.println(bCryptPasswordEncoder.matches(rawPassword, encPassword2));
}
}
암호화된 결과는 다르지만 rawPassword와 암호화된 값끼리는 매칭확인할 수 있습니다.
$2b$12$KlTV56khz1kIDSXU/YHrxeaeEffOCn.J57Ys2hlhi3Cx3HZkAj.Bi
$2b$12$Ut6zyHF0uvMn6kRIJunw0ewax2YqypjTk17tP930exx5fGNmj7EBO
true
true
출처: https://velog.io/@hyeinisfree/SpringSecurity-PasswordEncoder
[SpringSecurity] PasswordEncoder
Spring Security에서 지원하는 비밀번호 단방향 암호화 인터페이스Spring Security에서는 비밀번호를 안전하게 저장할 수 있도록 비밀번호의 단방향 암호화를 지원하는 PasswordEncoder 인터페이스와 구현
velog.io
출처:https://blog.jiniworld.me/172
[JCA] Hash 함수의 개요와 PBKDF2를 이용한 단방향 해시 알고리즘 구현
Hash Algorithm Hash 함수? MessageDigest 알고리즘 Avalanche effect MessageDigest의 단점 MessageDigest 해시함수 보완 방법 Adaptive Key Derivation Function PBKDF2 bcrypt 1. Hash Algorithm 1.1. Hash 함수? 해시 함수(= 해시 알고리즘)
blog.jiniworld.me
'Computer Science > Spring Security' 카테고리의 다른 글
Spring Security 사용자 관리 (0) | 2023.06.25 |
---|