단방향 암호화 아는척하기(1) - 이론편

Nine-JH·2023년 8월 1일
0

암호화 기능 추가해주세요!

과제에서 암호화 요구사항을 전달받았습니다. 사실 저는 Spring-Security를 애용하는 편이라 무지성 BCryptPasswordEncoder를 사용했지만, 정작 이게 정확하게 무엇인지는 알아본 적이 없더군요. 그래서 우선 단방향 알고리즘이 무엇인지에 대해 알아보도록합시다.

단방향 암호화

먼저 단방향 암호화에 대해서 알아볼 필요성이 있습니다. 복잡한건 여기서 다루지 말고 쉽게 설명만 하자면,

암호화(원문 -> 암호문)는 가능하지만 복호화(암호문 -> 원문)은 불가능한 방식을 말합니다.
이러한 특성 때문에 복호화 시 치명적인 문제를 발생시키는 정보들을 주로 단방향 암호화로 저장합니다.
주로 비밀번호가 대표적인 예시입니다. (보통 사람들은 여러 서비스 계정에 동일하거나 비슷한 암호를 쓰니깐요!)
곰곰히 생각해보시면 여러 서비스에서 비밀번호 찾기 기능을 수행할 때 실제로 비밀번호를 주는 것이 아니라 임시 비밀번호를 발급하거나, 비밀번호를 변경하는 권한을 부여하는 식으로 작동하는 것을 볼 수 있습니다. 복호화가 불가능 하니 이러한 방식으로 우회를 하는 것입니다.

단방향의 문제점

1. 동일한 암호문 (RainbowTable Attack)

위에서 단방향 암호화를 실시를 했지만 문제점이 하나 있습니다. 바로, 평문이 동일하면 암호문 역시 동일하다는 것입니다.
엄청 어려운 암호화 알고리즘을 사용해 암호화를 해보았다고 칩시다. 암호문으로 평문을 유추하기는 어렵지만, 매번 Apple을 암호화할때마다 같은 값으로 암호화가 된다면 어떻게 될까요? 아래와 같이 말이죠

이러면 이제 공격자들이 평문과 암호문을 매칭하는 테이블을 만들 수 있습니다. 마치 대학교의 시험 족보와 같은 느낌으로 말이죠. 이를 레인보우 테이블이라고 합니다.

위의 방식이라면 단방향 암호화는 아무리 어려운 알고리즘을 적용한다 해도 취약점이 명확해집니다. (물론 이상태도 풀긴 어렵습니다만..)

CrackStation

Salt의 중요성을 알아보기 위해 실제로 Rainbow_Table을 제공해주는 crackstation.net에서 테스트를 해봅시다.

void shaTest() throws NoSuchAlgorithmException {
    StringBuilder sb = new StringBuilder();
    String rawPassword = "apple12345";

    MessageDigest md = MessageDigest.getInstance("SHA-256");
    md.update(rawPassword.getBytes());

    byte[] digest = md.digest();
    for (byte b : digest) {
        sb.append(String.format("%02x", b));
    }
    System.out.println(sb.toString());
}

SHA256 암호화를 진행한 후 위의 홈페이지에서 돌려보면??

자비없이 뚫어버리는 모습을 볼 수 있습니다.!!

물론 RainbowTable은 족보의 개념이기 때문에 여기에 없는 데이터라면 안전하겠죠? (아래는 asldkjaasd를 암호화 한 해시를 테스트해보았습니다.)
asldkjaasd를 암호화 한 값


2. 무차별 대입(Bruteforce attack)

코딩테스트를 공부해보신 분들이라면 익숙하실 Bruteforce 입니다. 이 공격법은 간단합니다. 컴퓨터의 연산력을 믿고 무차별 대입하는 것이죠.

현대 단방향 암호화에 적용되는 해싱 알고리즘들을 완벽하게 해독하는데는 셀수도 없는 시간이 필요하다.
-네트워크 완벽 가이드(대충 이런 말이었음)-

나는 비밀번호를 사람들이 유추할 수 없이 정말로 어렵게 (아무 뜻이 없는 영문 + 숫자 + 특수문자) 형태로 쓴다면 걱정할 필요가 없겠지만 보통 사용자들은 상징적인 단어(이름, 생일, 어떤 단어 등등..)를 많이 사용하기 때문에 공격자가 조금이나마 이러한 단어들을 조합해서 무차별젹인 대입을 진행한다면 생각보다 쉽게 암호문을 얻을 수 있습니다.
개발자들이라면 이러한 상황을 당연히 대비를 해야 합니다!!






단방향 암호화 보완하기

위의 공격법들을 알았으니 이제 보완을 해봐야겠죠?
방법을 알아보기 이전에 명심해야 할 점은 이 보완법들은 공격을 원천차단하는 기술이 아닙니다. 그저 공격 난이도를 급격하게 상승시키는 방법일 뿐이죠. 그렇기 때문에 파훼가 될 것 같은데? 에 대한 걱정을 너무 깊게는 하지 않으셔도 됩니다.

1. Salt

해싱은 한 글자만 달라져도 값이 완전히 달라지게 됩니다.

 원문                                    암호문
Apple  -> f223faa96f22916294922b171a2696d868fd1f9129302eb41a45b2a2ea2ebbfd
Apple. -> da43ecd7455ab1d611618a965a5249731199857d086168df9cb885038a35a705

그렇다면 암호화 전 평문의 앞이나 뒤에 Random 값을 붙인 뒤 암호화를 하는 방법은 어떨까요? 쓸만해보이죠? 이것이 바로 Salt 기법입니다!

위와 같이 같은 암호일지라도 각각 다른 Salt 문자열을 가지며, 또 이를 추가적으로 암호화를 진행하기 때문에 암호문과 원문을 예측하기는 더더욱 어려워집니다. 이는 같은 원문이라도 Salt 문자열 가짓수에 비례해 수없이 많은 RainbowTable가 필요하다는 말이 되겠죠? (= 단일 RainbowTable을 사용할 수 없게 된다.)

Salt 주의사항

그렇다면 Salt 기법에서 가장 중요한 것은 뭐니뭐니해도 Salt 생성기(난수 생성기)겠죠? 이 생성기에서 뭐가 중요할까요?

  • 적어도 각각의 사용자는 고유한 Salt를 가져야 한다.
  • 추측이 어렵게 최소 NBit 이상이어야 한다. (최소 16Bit 이상이라고는 말합니다.)
  • 추후 암호문 비교를 위해서는 서버에서는 Salt를 반드시 어딘가에 저장해야 한다!
    • DB에 암호문과 Salt를 같이 저장
    • 암호문 자체에 Salt를 이어 붙여서 저장(이를 Padding이라고 합니다)



2. Key Stretching

인형안에 또 인형이 들어있는 마트료시카처럼 암호문을 또 암호화를 하는 기법을 말합니다.

Key Stretching 기법은 두가지 목적이 있습니다.

  • 여러번 암호화를 통해서 원문 - 암호문 간 예측이 어렵게 만들기
  • 설령 몇번 암호화를 했는지 알더라도 자원 소모율이 높아 브루트 포스 공격을 사용하기 난감하게 만들기.

PLUS. 얼마나 느려지는데요?

다음 포스트를 맛보기로 보여드리자면 BCrypt를 통해 2^102^20번 암호화를 돌린 결과물을 JMH를 통해 성능테스트를 실시한 결과물입니다.

Benchmark                          Mode   Cnt   Score     Error     Units
_02_BCryptTest.Test1.easyBCrypt    avgt   10   74.939    ± 1.384    ms/op
_02_BCryptTest.Test1.strongBCrypt  avgt   10   76895.705 ± 535.434  ms/op

극단적인 2^20의 경우 평균적으로 하나의 암호화를 하는데 76.895초가 걸리게 됩니다.
물론 기본값으로 테스트 한 2^10은 74.939ms가 걸리게 되는데 일반적으로 초당 10억번 이상을 실시 할 수 있는 해싱 알고리즘을 생각해본다면 적은 시간은 아니죠?

구현해보기

public class KeyStretchingEncoder implements PasswordEncoder {

    private static final int SALT_SIZE = 16;
    private final int strength;

    public SHAPasswordEncoder() {
        this.strength = 10;
    }

    public SHAPasswordEncoder(int strength) {
        this.strength = strength;
    }

    private int calcRound(int strength) {
        return 1 << strength;
    }

    @Override
    public String encode(String rawPassword) throws NoSuchAlgorithmException {
    	MessageDigest md = MessageDigest.getInstance("SHA-256");
        int round = calcRound(this.strength);
        byte[] passwordBytes = rawPassword.getBytes();
        
        for (int i = 0; i < round; i++) {
            md.update(passwordBytes);
            passwordBytes = md.digest();
        }
        StringBuilder sb = new StringBuilder();
        appendStrength(sb);
        
        for(byte a : temp) {
            sb.append(String.format("%02x", a));
        }
        
        return sb.toString();
    }
    
    private void appendStrength(StringBuilder sb) {
        sb.append("$");
        if(this.strength < 0) {
        	sb.append(0);
            sb.append(this.strength);
        } else {
            sb.append(this.strength);
        }
    }
}


정리

  • 단방향 암호화는 복호화가 불가능한 알고리즘이다. 그래서 주로 정말 중요한 정보에 사용된다. (ex: 비밀번호)
  • 단방향 암호화의 대표적인 예시는 해싱 알고리즘이 있는데, 이를 그대로 사용하면 평문이 동일하면 항상 암호문이 동일하기 때문에 브루트포스레인보우 테이블 공격에 취약해진다.
  • 이러한 문제점을 극복하기 위해서 SaltKey Stretching 기법을 사용한다.
    * Salt : 평문에 난수를 붙힌 뒤에 암호화를 실시해 평문을 예측하기 어렵게 만드는 방법
    • Key Stretching : 암호화를 여러번 실시하여 평문 예측과 높은 자원 소모로 브루트포스 공격을 사용하기 난감하게 만드는 방법
  • 명심해야 할 것은 보완법들은 공격을 원천차단하는 기술이 아닌, 레인보우 테이블이나 브루트포스 공격법의 난이도를 어렵게 하는 기술일 뿐이다.
  • 그렇기 떄문에 Salt가 공개되거나, rounds가 공개된다 하더라도 그리 치명적인 문제는 아니다. (그렇다고 관리를 소홀히 하지는 말자.)

다음 포스트에서는 SaltKey Stretching을 모두 적용한 BCrypt를 살펴보도록 하겠습니다.

2개의 댓글

comment-user-thumbnail
2023년 8월 1일

유익한 글이었습니다.

1개의 답글