최신 글
8
제목 게시일
37
Web

Hash(해시)를 이용한 안전한 패스워드 저장

profile
코우
2021-11-08 21:32
조회 수 : 9439

웹 애플리케이션을 만들 때 사용자 비밀번호와 같이 중요한 데이터들은 별도의 처리가 필요합니다.
보통 이러한 정보들을 데이터베이스에 저장하는데 만약 평문(비밀번호 그대로)를 저장하게 된다면 해킹에 취약해질 수 있습니다.
 
단방향 해시 함수

해시 함수란 임의의 길이의 평문(input)을 입력하면 고정 길이의 해시 값(digest)을 반환하는 함수입니다. 

해시 함수는 입력 값이 같으면 출력 값도 항상 같다는 특징이 있습니다. 이러한 특징으로 해시를 패스워드 저장에 활용할 수 있습니다.
 
SHA-256


SHA-256은 미국의 국립표준기술연구소(NIST)에 의해 공표된 표준 해시 알고리즘인 SHA-2 계열 중 하나입니다.
이름에서 알 수 있듯이 SHA-256은 256Bit로 구성되며 16진수로 구성된 64자리 문자열을 반환합니다.

설명 : 16진수는 4Bit로 표현할 수 있습니다. 따라서 256Bit -> 64개의 16진수를 표현할 수 있습니다.
 
패스워드 저장

해시를 사용하여 비밀번호를 저장하는 웹 서비스의 경우 웹 서비스에서 비밀번호를 찾을 때 사용자에게 비밀번호를 알려주지 않고 새로운 비밀번호로 변경하도록 합니다. 이는 사용자가 비밀번호를 분실하면 해시 알고리즘의 특성상 찾아내기 매우 어렵기 때문입니다. 

신규 및 변경
  • 비밀번호 평문 -> 해시 함수 -> 해시 값 -> DB에 저장
인증(로그인)
  • 비밀번호 평문 -> 해시 함수 -> 해시 값 -> DB에 저장된 해시 값과 비교

취약성

해시를 사용해서 비밀번호를 저장하면 보안성에 도움이 되긴 하지만 문제점이 있습니다.
그렇다면 문제점이 무었이 있는지 알아보도록 합시다.

웹 서비스들을 보시면 특수문자, 대소문자, 글자 길이 등 여러 가지 요소를 포함하여 비밀번호를 만들기를 권장하고 있습니다. 그 이유는 해시의 특징인 동일한 입력 값에 대해 항상 동일한 해시 값을 반환한다는 점 때문입니다. 


1. 레인보우 테이블

해시 함수와 반대로 해시 값을 입력 값으로 변환하는 레인보우 테이블(Rainbow Table)이 있습니다.
레인보우 테이블은 해시 함수가 만들어 낼 수 있는 (입력 값,해시 값) 쌍을 대량으로 저장해놓은 테이블인데 단순한 입력 값에 대한 해시 값은 레인보우 테이블을 통해 입력 값을 알아낼 수 있습니다.


2. 무차별 대입

SHA-256의 경우 2256의 경우의 수를 가집니다. 이는 엄청나게 큰 수로 모든 경우의 수를 대입해보는 것은 현재 컴퓨터의 연산 속도로는 사실상 불가능합니다.

 
보완

레인보우 테이블로도 찾을 수 없는 복잡한 입력 값을 넣는 것이 하나의 방법입니다. 
대표적으로 Salt와 키 스트레칭(Key Stretching)이 있습니다. 

1. Salt

말 그대로 소금을 치는 작업입니다. 입력 값에 랜덤 문자열을 추가하여 해시 함수에 넣는 방법입니다. 
랜덤 문자열이 추가되기 때문에 사용자가 아무리 단순한 값을 입력해도 해시 함수에 들어가는 값은 복잡해집니다.




2. Key Stretching

해시 동작을 N번 반복하는 작업입니다.


동작
1. 입력 값에 대한 해시 값을 얻는다.
2. 얻은 해시 값을 입력 값으로 지정한다.
3. 1,2번 동작을 N번 반복한다.



Salt와 Key Stretching을 함께 사용하면 더욱 강력하게 패스워드를 보호할 수 있습니다.
 
구현
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import java.security.MessageDigest;
import java.security.SecureRandom;
 
public class SecureTool {
    private static final int STRETCH_SIZE = 1000;        //스트레칭 횟수
    private static final int SALT_SIZE = 64;            //Salt값 길이
    
    //Byte배열을 16진수 문자열로 변환
    private String byteToString(byte[] digest) {
        StringBuilder sb = new StringBuilder();
 
        for (byte b : digest) {
            sb.append(String.format("%02x", b));    // 1Byte = 8Bit = 16진수 2자리
        }
 
        return sb.toString();
    }
    
    // 비밀번호 + Salt값에 대한 해시 값을 반환
    public String getHashValue(String password,String salt) throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
 
        for (int i = 0; i < STRETCH_SIZE; i++) {
            String temp = password + salt;
            md.update(temp.getBytes());
            password = byteToString(md.digest());
        }
 
        return password;
    }
    
    //Salt값 생성
    public String createSalt() {
        return makeRandomString(SALT_SIZE);
    }
    
    // 특정 길이만큼의 임의의 문자열 생성
    public String makeRandomString(int length) {
        SecureRandom secureRandom = new SecureRandom();
        byte[] temp = new byte[length];
 
        secureRandom.nextBytes(temp);
 
        return byteToString(temp);
    }
}
cs
share
twitter facebook kakao naver
댓글