[Security] 비밀번호 단방향 암호화

17260 단어 SpringbootSpringboot

단방향 암호화(One-Way Encryption)

단방향 암호화란 한쪽 방향으로만 암호화를 한다는 의미이다. 즉 암호화만 가능하고 복호화는 할 수 없다.
그렇기 때문에 비밀번호를 관리할 때 유용하게 사용된다.
비밀번호를 단방향 암호화 방식으로 저장하는 경우에는 패스워드 DB가 노출되어도 안전하다.
패스워드를 검증할 때에는 사용자로부터 입력받은 비밀번호를 똑같은 방식으로 암호화하여 암호화된 패스워드끼리 비교를 하면 된다.

유저가 비밀번호를 잊어버렸을 때는 찾기가 불가능하다.
대신 비밀번호 변경 메일, SMS 인증을 통해 새로운 비밀번호를 입력하도록 하면 된다.

대표적으로 많이 사용하고 있는 알고리즘은 SHA-256 암호화 알고리즘이다.

단방향 암호화 종류

해시(Hash) 알고리즘을 사용한다.

  • MD5(Message-Digest algorithm 5)
    • 128 비트 암호학 해시 함수
    • 현재는 안전하지 않아 사용하지 않는다. (결함을 이용해 SSL 인증서 변조 가능)
  • SHA-1 : 안전 X (SHA-0과 SHA-1에 대한 공격은 이미 발견)
  • SHA-2(SHA-256) : 권장

SHA-256 알고리즘

임의의 길이 메세지를 256 bits의 축약된 메세지로 만드는 알고리즘이다.
경우의 수가 2^256 이므로 무차별 대입을 통한 공격에 비교적 안전하다.
(SHA-512의 경우의 수는 2^512)

SHA-256은 현재 가장 많은 분야에서 채택하여 사용되고 있는 암호 방식이다.
속도가 빠르기 때문에 인증서, 블록체인 등 많이 사용되고있으며, SHA-2라고 하면 SHA-256이라고 말할 정도로 상용화가 잘 되어있다

SHA-256 알고리즘을 알아봤으니 간단한 실습을 해보겠다.

  • dog 문자열 SHA-256으로 암호화
 CD6357EFDD966DE8C0CB2F876CC89EC74CE35F0968E11743987084BD42FB8944
  • dot 문자열 SHA-256으로 암호화
 E392DAD8B08599F74D4819CD291FEEF81AB4389E0A6FAE2B1286F99411B0C7CA

마지막 글자만 바꿨을 뿐인데, 전혀 비슷한 단어라고 생각할 수 없는 값이 나온다.
즉, 사용자의 raw password를 추론하기 어렵다는 것을 알 수 있다.

단방향 해시 함수의 한계점

Rainbow Table (레인보우 테이블)

동일한 메세지는 동일한 다이제스트를 갖는다.

다이제스트 : 해시 함수라는 수학적인 연산을 통해 생성된 암호화된 메시지를 의미한다.

위에서 입력했던 "dog"를 예시로 들어보면 다이제스트는 항상 같은 값을 갖는다.

CD6357EFDD966DE8C0CB2F876CC89EC74CE35F0968E11743987084BD42FB8944

Rainbow table은 해시 함수를 사용하여 변환 가능한 모든 해시 값을 저장시켜 놓은 표이다. 보통 해시 함수를 이용하여 저장된 비밀번호로부터 원래의 비밀번호를 추출해 내는데 사용된다. (다이제스트들의 테이블)

이렇게 사용자들이 많이 사용하는 password 나 복잡하지 않은 암호의 경우 이미 공격자들이 대입해보았을 확률이 높다.
그리고 대입해 볼 확률이 높다는 것은 즉, 이미 해당 패스워드의 다이제스트가 레인보우 테이블에 있을 가능성이 높다는 것이다.

CrackStation 사이트

"dog"의 다이제스트를 넣어보자

"dog" 결과가 나오는 것을 알 수 있다.
정말 랜덤하게 "randomrandomrandomrandom"의 다이제스트를 대입해보겠다.

368C8FF29498B255C4FEE5AB93702897260F4B42EEF191BE4361DDFC9CBE906B

결과 : Not Found
즉 많이 사용하지 않거나 복잡한 패스워드임을 알 수 있다.

👉 보완 : Salting (위에서 언급한 BCrypt에 포함된 방법)

패스워드에 임의의 문자열인 salt를 추가하여 다이제스트를 생성한다.
같은 패스워드라도 각기 다른 salt가 들어가 다이제스트가 다르게 생성되어 rainbow table을 무의미하게 만든다.
salt는 최소 128bit 정도는 되어야 안전하다.
이미지 출처 링크

Brute Force 공격 (무차별 대입 공격) - 속도

해시 함수는 원래 빠른 데이터의 검색을 목적으로 설계된 것이다. 그렇다보니 해시 함수를 써도 원문의 다이제스트는 금방 얻어진다.
해시 함수는 원래 짧은 시간에 데이터를 검색하기 위해 설계된 것으로 해시함수의 빠른 처리 속도로 인해 해커는 매우 빠른 속도로 임의의 문자열의 다이제스트와 해킹할 대상의 다이제스트를 비교할 수 있다.
: 빠른 속도의 해싱 함수가 문제점이다.

👉 보완 : 키 스트레칭

해시를 여러 번 반복하여 시간을 늘림으로써 무차별 대입 공격(brute force attack)에 대비한다. 패스워드의 다이제스트를 생성하고, 생성된 다이제스트를 입력값으로 하여 또 다이제스트를 생성하는 것을 반복한다.


스프링부트로 비밀번호 암호화 적용해보기

단방향 암호화로 비밀번호를 입력해보자
Spring Security 패키지에 인증/권한 인가 등의 처리 등을 지원한다.

PasswordEncoder 인터페이스

public interface PasswordEncoder {
    String encode(CharSequence rawPassword); // 패스워드 암호화

    boolean matches(CharSequence rawPassword, String encodedPassword); // 입력 받은 패스워드와 비교

    default boolean upgradeEncoding(String encodedPassword) { // default 메서드로 구현된 upgradeEncoding은 기본적으로 false를 리턴하지만, 커스텀하게 구현할 경우 더 강력한 암호화를 실행할 것인지에 대한 로직 처리
        return false;
    }
}

PasswordEncoder 구현 클래스

  • BCryptPasswordEncoder
    • BCrypt 라는 해시 함수를 사용한 구현체(BCrpyt는 애초부터 패스워드 저장을 목적으로 설계)
    • salt를 넣는 작업까지 하므로, 입력값이 같음에도 불구하고 매번 다른 encoded된 값을 return 한다.
	// encode 함수
    
  	@Override
	public String encode(CharSequence rawPassword) {
		if (rawPassword == null) {
			throw new IllegalArgumentException("rawPassword cannot be null");
		}
		String salt = getSalt();
        // salt를 생성한 후, rawPassword와 salt를 이용해 해싱한다.
		return BCrypt.hashpw(rawPassword.toString(), salt);
	}

	private String getSalt() {
		if (this.random != null) {
			return BCrypt.gensalt(this.version.getVersion(), this.strength, this.random);
		}
		return BCrypt.gensalt(this.version.getVersion(), this.strength);
	}
       // matches 함수
       
	@Override
	public boolean matches(CharSequence rawPassword, String encodedPassword) {
		if (rawPassword == null) {
			throw new IllegalArgumentException("rawPassword cannot be null");
		}
		if (encodedPassword == null || encodedPassword.length() == 0) {
			this.logger.warn("Empty encoded password");
			return false;
		}
		if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
			this.logger.warn("Encoded password does not look like BCrypt");
			return false;
		}
		return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
	}
    @Test
    void 비밀번호_암호화_테스트() {
        String input = "test";
        String encoded = passwordEncoder.encode(input);

        // true
        assertThat(passwordEncoder.matches(input, encoded), is(true));

        // false
        // 입력한 비밀번호가 같더라도 다른 값이 나온다.
        assertThat(passwordEncoder.encode(input), is(not(passwordEncoder.encode(input))));
    }

왜 SHA가 아니라 BCrypt?

SHA-2, SHA-3은 너무 빠른 것이 단점이다.
해싱 함수는 느리면 좋다. 왜냐하면 비밀번호 크래킹을 어렵게 할 수 있기 때문이다.
해시 함수를 매우 느리게 만들면, 빠른 GPU나 커스텀 하드웨어를 이용해도 사전 공격이나 브루트포스 공격이 쓸모 없어질 만큼 느려진다.
(학교에서 배웠던 내용 그대로당.. 신기)

Bcrypt 설계자들은 이런 문제로 SHA가 아닌 Blowfish를 이용하여 구현했다. 이는 반복 횟수를 변수로 지정 가능하게 하여 작업량(즉, 해싱 시간)을 조절할 수 있게 하였다. 이는 원하는 만큼 속도가 조절 가능한 해시 함수인 것이다.

BCryptPasswordEncoder는 salt까지 포함하니 보안성이 더 높아진다.

이 외의 구현체들

  • DelegatingPasswordEncoder
    • PasswordEncoder를 여러개 선언한 뒤, 상황에 맞게 골라쓸 수 있도록 지원하는 Encoder
  • SCryptPasswordEncoder
    • SCrypt 라는 해시 함수를 사용한 구현체
    • SCrypt는 다이제스트를 생성할 때 메모리 오버헤드를 갖도록 설계되어, 브루트포스 공격을 시도할 때 병렬화 처리가 매우 어렵다.
  • Pbkdf2PasswordEncoder
    • PBKDF2 라는 해시 함수를 사용한 구현체
    • PBKDF2는 NIST(National Institute of Standards and Technology, 미국표준기술연구소)에 의해서 승인된 알고리즘

좋은 웹페이지 즐겨찾기