개요
실제 서비스를 개발해 본 경험이 있는 개발자라면 사용자의 비밀번호를 DB에 평문으로 저장하면 절대 안 된다는 것을 알 것입니다.
그 이유는 보통 사람들은 여러 사이트에 동일한 아이디와 비밀번호를 사용하는데 혹여나 DB가 해킹당했을 경우 해커가 평문으로 저장된 아이디와 비밀번호를 통해 여러 사이트를 동시에 접속할 가능성이 있기 때문입니다.
이 때문에 비밀번호는 항상 암호화를 한 뒤 저장해줘야하는데 이번 게시글에서는 해싱 즉, 단방향 암호화 기능을 제공하는 BcryptPasswordEncoder에 대해 간단히 알아보고 단방향 암호화에 대해서도 간단하게 알아보겠습니다.
좋은 글을 작성하고 공유해주신 Stranger's LAB님과 Kim VamPa님께 감사드립니다.
1. BcryptPasswordEncoder
BcryptPasswordEncoder는 PasswordEncoder 인터페이스의 구현체이며 Bcrypt 해싱 함수를 사용해 비밀번호를 인코딩해주는 메서드와 사용자가 로그인할 때 제출한 비밀번호와 DB에 저장되어 있는 비밀번호의 동일 여부를 확인해주는 메서드를 제공합니다.
1.1 BcryptPasswordEncoder 생성자
BcryptPasswordEncoder의 생성자를 보면 인자값으로 version, strength, SecureRandom 인스턴스를 받는데 해당 인자들을 통해 해시의 강도를 조절할 수 있습니다.
strength는 디폴트로 10이며 이는 후술할 salt의 길이를 의미합니다.
1.2 BcryptPasswordEncoder 메서드
String encode(CharSequence rawPassword)
- 패스워드를 암호화해주는 메서드이며 해당 메서드는 SHA-2 이상의 알고리즘, 8바이트로 결합된 해쉬, 그리고 랜덤 하게 생성된 솔트를 지원 (SHA-1은 Hash 충돌이 발견됐기 때문에 사실상 퇴출)
- 매개변수로 CharSequence 타입의 데이터를 넣어주면 되며 String, StringBuffer, StringBuilder 등이 이에 해당
- 반환 타입은 String 타입
- 똑같은 비밀번호를 입력하더라도 해당 메서드는 salt를 결합한 뒤 인코딩을 하기 때문에 매번 다른 결과가 나옴
boolean matches(CharSequence rawPassword, String encodePassword)
- 입력된 평문 패스워드와 encode 메서드를 통해 인코딩된 패스워드의 동일 여부 파악에 사용
- 첫 번째 인자로 평문 패스워드, 두 번째 패스워드로 인코딩된 패스워드
- 반환 타입은 boolean 타입
boolean upgradeEncoding(String encodePassword)
- 인코딩 된 패스워드가 안전한지 파악하는 함수
- 매개변수로 인코딩 된 패스워드를 넣어주면
- 반환 타입은 boolean 타입이며 안전하다면 false, 안전하지 못해서 추가적으로 인코딩해야 한다면 true 반환
- encode() 메서드를 통해 인코딩된 암호들은 기본적으로 안전하다고 판단하여 false 반환
- 다른 함수를 통해 인코딩된 암호의 안전 여부를 파악하는 데 사용하면 좋을 것 같음
1.3 적용 방법
1. pom.xml 혹은 gradle에 spring security 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2. Config 내 빈으로 PasswordEncoder 등록
* 여기서 명시적으로 BcryptPasswordEncoder를 등록하지 않고 PasswordEncoder를 등록한 이유는 디폴트 PasswordEncoder가 Bcrypt + salt를 사용하기 때문입니다.
* 따라서, 위와 같이 등록하면 upgradeEncoding 메서드는 사용 불가 (사실 필요성을 잘 못 느끼겠음...)
3. 실제 메서드 호출
* Bcrypt 해싱이 디폴트인 것을 확인할 수 있습니다.
2. 단방향 해시 함수
2.1 단방향 해시 함수란?
지금까지 알아본 PasswordEncoder는 비밀번호를 해싱하므로 암호화만 가능한 단방향 해시 함수입니다.
해싱이 단방향인 이유는 암호화는 되지만 복호화는 안되기 때문입니다.
해시에 의해 암호화된 데이터를 다이제스트(digest)라고 부르며 다시 복습하자면 해싱을 통해 평문에서 다이제스트로 암호화는 되지만 다이제스트에서 평문으로 복호화는 불가능합니다.
이러한 특성 때문에 개요에서도 설명했다시피 DB가 해킹을 당하더라도 유저들의 패스워드가 해싱이 되어있다면 해킹에 덜 취약하다고 할 수 있습니다.
2.2 단방향 해시 함수의 종류
단방향 해시 함수의 종류는 다양하며 아래는 대표적인 4가지 해싱 함수입니다.
- SHA
- MD
- HAS
- WHIRLPOOL
앞서 말했다시피 SHA-0, SHA-1은 해시 충돌이 발견했기 때문에 더 이상 안 쓰이고 현재는 SHA-256이 자주 쓰이고 있습니다.
해싱이 비교적 안전하다는 것을 증명하기 위해 20대 남자라면 아는 비밀번호인 q1w2e3r4에 SHA-256 알고리즘을 적용해보겠습니다.
q1w2e3r4
13A5C202E320D0BF9BB2C6E2C7CF380A6F7DE5D392509FEE260B809C893FF2F9
q1w2e3r4!
40C0BB054BF07D5C614C8AA3C827CE5DA20EAF4C04A338F344B9BF91505C6CCE
이처럼 두 비밀번호는 ! 차이밖에 안나지만 전혀 다른 다이제스트가 나왔고 그만큼 해커가 다이제스트를 통해 원문 비밀번호를 파악하기 힘들다는 것을 의미합니다.
2.3 단방향 해시 함수의 한계
위에서 작성한 예시처럼 SHA-256 알고리즘과 같은 단방향 해시 함수를 적용하면 DB가 해킹되더라도 안전하다고 생각할 수 있지만 아래의 문제점들 때문에 현실은 그렇지 않습니다.
같은 비밀번호는 동일한 다이제스트를 생성
- 다이제스트를 직접 복호화하는 것은 어렵지만 같은 비밀번호는 동일한 다이제스트를 생성한다는 특성을 이용해 해커가 직접 값을 대입하면서 다이제스트와 비교하는 방식을 가져가면 보안이 취약해짐
- 해커들이 여러 값들을 대입하면서 얻은 다이제스트들을 모아놓은 리스트를 레인보우 테이블(Rainbow Table)이라고 하며 이는 인터넷에서 쉽게 접근할 수 있음 (당장 SHA-256 Rainbow Table을 검색하면 방문자님의 비밀번호에 대한 digest가 나와있을 수도 있습니다...)
- 따라서, 복잡하지 않은 비밀번호로 회원가입을 했을 경우 해킹에 매우 취약
무차별 대입 공격 (Brute Force)
- 해시 함수는 기본적으로 빠른 데이터 검색을 위한 목적으로 설계되어있기 때문에 짧은 시간 안에 해시 함수를 통해 다이제스트를 구할 수 있음
- 해커 입장에서는 무차별적으로 모든 암호를 대입하여 다이제스트를 구한 뒤 레이보우 테이블을 업데이트하면 됨
- 물론 양자 컴퓨터가 아니고서야 모든 경우의 수를 파악하기 어렵지만 사람들은 보통 자기가 기억할 수 있는 비교적 짧고 간단한 비밀번호를 사용하는 경향이 있기 때문에 브루트 포스 공격으로도 충분히 뚫릴 확률이 있음
- 이 때문에 요즘 사이트들이 비밀번호를 특수문자 포함 8자리 이상으로 설정하라고 하는 것
2.4 단방향 해시 함수 보완
이처럼 단방향 해시 함수가 완벽하지 않기 때문에 보완을 해야 하는데 유명한 방법으로는 Key Stretching과 salt 기법이 있습니다. (BcryptPasswordEncoder가 salting 기법을 사용)
Key Stretching
- 원문 패스워드를 여러 번 해싱하는 기법
- 기존 예제를 사용하자면 q1w2e3r4를 해싱하면 다이제스트로 13A5C202E320D0BF9BB2C6E2C7CF380A6F7DE5D392509FEE260B809C893FF2F9이 나오고 이를 다시 해싱하면 다이제스트로 8E1D67BC07AACD194B09994DF5B6B14143B62B5FF983E702822EBEAAC4027342이 나옵니다.
- 이처럼 개발자가 설정한 횟수만큼 SHA-256 기법을 반복하면 원문으로부터 다이제스트를 구하는 데 걸리는 시간이 오래 걸리기 때문에 해커가 레인보우 테이블을 생성하는데 시간 제약이 생김
- 브루트 포스 공격을 무력화하는데 효과적인 방법
Salting
- 해커가 Key Stretching이 반복하는 횟수를 파악할 경우 대표적인 문자열들을 추려서 대입하는 방식으로 시간 제약을 극복할 수 있고 결국에는 같은 비밀번호를 사용하는 사용자들이 다수라면 Key Stretching 기법을 사용해도 보안에 취약하기 때문에 나온 방법이 salting 기법
- salting이란 해시 함수를 돌리기 전에 원문에 임의의 문자열을 덧붙이는 것을 말함
- salting 기법을 사용하면 digest를 알아도 원문을 알기 어려우며 사용자마다 다른 salt를 부여할 경우 같은 비밀번호여도 다이제스트의 값이 달라 한 명의 비밀번호가 유출되더라도 동일한 비밀번호를 사용하는 다른 사용자들은 비교적 안전함
- salt는 임의의 문자열이므로 레인보우 테이블에 존재하지 않을 가능성이 높음
- salt의 길이는 32비트 이상이 되어야 salt와 digest를 추측하기 어렵다고 함
Key Stretching + Salting
- 패스워드에 salt를 추가하여 인코딩해서 구한 다이제스트에 대해 또 salt를 추가하여 인코딩하고.... 이런 식으로 반복하면 보다 안전함
참고
https://st-lab.tistory.com/100
https://kimvampa.tistory.com/129
'Spring' 카테고리의 다른 글
Spring Boot 개요 (0) | 2023.02.27 |
---|---|
[SpringBoot] 구글 SMTP 통해 메일 보내기 (2) | 2022.04.10 |
[SpringBoot] 로그인된 사용자가 접근할 수 있는 기능 Test 작성하는 방법 (0) | 2022.04.02 |
[SpringBoot] OpenSessionInView (0) | 2022.03.29 |
[SpringBoot] AOP 간단 정리 (0) | 2022.01.12 |