본문 바로가기

Backend/Java&Spring

아직도 Bcrypt만 쓰시나요? 이제는 Argon2id로 전환해야 할 때 (OWASP 권장)

1. 왜 다시 패스워드 암호화인가?

 보안을 위해서 패스워드를 평문 그대로 저장하지 않고 해싱(Hashing)해야 한다는 것은 개발자라면 누구나 알고 있는 상식입니다. 그동안 우리는 Bcrypt나 Scrypt 같은 알고리즘을 대중적으로 사용해 왔습니다.

하지만 하드웨어(GPU, ASIC)의 성능이 비약적으로 발전하면서, 기존 알고리즘들에 대한 보안 위협이 커지고 있습니다. 그래서 오늘은 2015년 'Password Hashing Competition'에서 우승하고, 2023년 OWASP에서 권장 1순위 알고리즘으로 지정한 Argon2id로 전환하는 방법을 공유하려 합니다.

2. 왜 Bcrypt가 아니라 Argon2id인가?

 가장 큰 차이는 '메모리 하드(Memory-hard) 함수'라는 점에 있습니다.

  • Bcrypt의 한계: 주로 CPU 연산 능력에만 의존합니다. 최근 고성능 GPU를 이용한 병렬 연산 공격(무차별 대입)에 취약해지는 문제가 있습니다.
  • Argon2id의 강점: 연산 능력뿐만 아니라 메모리 사용량을 강제합니다. 공격자가 수만 개의 패스워드를 동시에 대입하려면 엄청난 양의 RAM이 필요하게 되어, 공격에 필요한 하드웨어 비용을 기하급수적으로 높입니다.

 

3. Spring Boot에 Argon2id 적용하기

 저도 기존 프로젝트에 사용하던 Bcrypt 해싱 알고리즘을 Argon2 Password Hashing 방법으로 변환하기 위해서 우선 build.gradle에 Bouncy Castle 라이브러리를 의존성에 추가합니다.

implementation 'org.bouncycastle:bcprov-jdk18on:1.77'

 

 그리고 기존에 SecurityConfig에서 사용하던 방식인 Bcrypt에서

@Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
 }

 

 Argon2 방식으로 passwordEncoder를 수정하였습니다.

@Bean
    public PasswordEncoder passwordEncoder() {
        return new Argon2PasswordEncoder(
                16,     // saltLength
                32,     // hashLength
                1,      // parallelism
                65536,  // memory (64MB)
                3       // iterations
        );
    }
  • saltLength (16): 솔트의 길이(byte)입니다. 16바이트(128비트) 정도면 레인보우 테이블 공격을 방어하기에 충분합니다.
  • hashLength (32): 생성될 해시의 길이(byte)입니다. 보통 32바이트(256비트)를 사용합니다.
  • parallelism (1): 해시 계산 시 사용할 스레드(Thread) 수입니다. 서버의 CPU 코어 여유에 따라 조절하지만, 보통 1~2를 권장합니다.
  • memory (65536): 해시 연산에 사용할 메모리 양(KiB 단위)입니다. 65536은 64MB를 의미하며, Argon2의 핵심 방어 기제입니다.
  • iterations (3): 연산을 몇 번 반복할지 결정합니다. 숫자가 높을수록 보안은 강해지지만 로그인이 느려집니다.


 

 주의해야 할 것은 알고리즘을 변경하면 기존에 Bcrypt로 저장된 비밀번호로는 로그인이 안 됩니다. 이를 해결하기 위해 Spring Security는 여러 암호화 방식을 동시에 지원하는 DelegatingPasswordEncoder를 제공합니다.

 

SecurityConfig 수정 예시

@Bean
public PasswordEncoder passwordEncoder() {
    String idForEncode = "argon2"; // 새로 생성되는 비밀번호는 argon2로 암호화
    
    // 1. 지원할 인코더 목록 생성
    Map<String, PasswordEncoder> encoders = new HashMap<>();
    
    // 기존 사용자를 위해 Bcrypt 유지
    encoders.put("bcrypt", new BCryptPasswordEncoder());
    
    // 신규 적용할 Argon2 설정
    encoders.put("argon2", new Argon2PasswordEncoder(16, 32, 1, 65536, 3));

    // 2. DelegatingPasswordEncoder 반환
    // DB에 저장된 패스워드 앞의 식별자( {bcrypt}, {argon2} )를 보고 판단함
    return new DelegatingPasswordEncoder(idForEncode, encoders);
}

 

어떻게 동작하나요? (마이그레이션 전략)

  1. 동시 지원: 이제 DB에 저장된 비밀번호가 {bcrypt}$2a$10$... 형식이면 Bcrypt로 검증하고, {argon2}$argon2id$... 형식이면 Argon2로 검증합니다.
  2. 자동 업데이트 로직 (권장): 로그인 성공 시점에 기존 Bcrypt 사용자인지 체크하여, 맞다면 새로운 Argon2로 다시 해싱해서 DB를 업데이트하는 로직을 추가하면 완벽합니다.
// 로그인 서비스 로직 예시 (의사 코드)
if (passwordEncoder.matches(rawPassword, encodedPassword)) {
    // 로그인은 성공했지만, 만약 Bcrypt로 되어있다면?
    if (encodedPassword.startsWith("{bcrypt}")) {
        // 새 알고리즘(Argon2)으로 다시 암호화해서 DB 업데이트
        String newPassword = passwordEncoder.encode(rawPassword);
        userRepository.updatePassword(userId, newPassword);
    }
    return true;
}

 

 "단순히 알고리즘을 바꾸는 것을 넘어, 기존 사용자의 불편함 없이 보안 수준을 높이는 것이 진정한 운영의 묘미입니다. DelegatingPasswordEncoder를 활용해 점진적으로 Argon2id로 전환해 보시기 바랍니다."