과제에서 암호화 요구사항을 전달받았습니다. 사실 저는 Spring-Security를 애용하는 편이라 무지성 BCryptPasswordEncoder
를 사용했지만, 정작 이게 정확하게 무엇인지는 알아본 적이 없더군요. 그래서 우선 단방향 알고리즘이 무엇인지에 대해 알아보도록합시다.
먼저 단방향 암호화에 대해서 알아볼 필요성이 있습니다. 복잡한건 여기서 다루지 말고 쉽게 설명만 하자면,
암호화(원문 -> 암호문)는 가능하지만 복호화(암호문 -> 원문)은 불가능한 방식을 말합니다.
이러한 특성 때문에 복호화 시 치명적인 문제를 발생시키는 정보들을 주로 단방향 암호화로 저장합니다.
주로 비밀번호가 대표적인 예시입니다. (보통 사람들은 여러 서비스 계정에 동일하거나 비슷한 암호를 쓰니깐요!)
곰곰히 생각해보시면 여러 서비스에서 비밀번호 찾기 기능을 수행할 때 실제로 비밀번호를 주는 것이 아니라 임시 비밀번호를 발급하거나, 비밀번호를 변경하는 권한을 부여하는 식으로 작동하는 것을 볼 수 있습니다. 복호화가 불가능 하니 이러한 방식으로 우회를 하는 것입니다.
위에서 단방향 암호화를 실시를 했지만 문제점이 하나 있습니다. 바로, 평문이 동일하면 암호문 역시 동일하다는 것입니다.
엄청 어려운 암호화 알고리즘을 사용해 암호화를 해보았다고 칩시다. 암호문으로 평문을 유추하기는 어렵지만, 매번 Apple을 암호화할때마다 같은 값으로 암호화가 된다면 어떻게 될까요? 아래와 같이 말이죠
이러면 이제 공격자들이 평문과 암호문을 매칭하는 테이블을 만들 수 있습니다. 마치 대학교의 시험 족보
와 같은 느낌으로 말이죠. 이를 레인보우 테이블
이라고 합니다.
위의 방식이라면 단방향 암호화는 아무리 어려운 알고리즘을 적용한다 해도 취약점이 명확해집니다. (물론 이상태도 풀긴 어렵습니다만..)
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
를 암호화 한 해시를 테스트해보았습니다.)
코딩테스트를 공부해보신 분들이라면 익숙하실 Bruteforce
입니다. 이 공격법은 간단합니다. 컴퓨터의 연산력을 믿고 무차별 대입하는 것이죠.
현대 단방향 암호화에 적용되는 해싱 알고리즘들을 완벽하게 해독하는데는 셀수도 없는 시간이 필요하다.
-네트워크 완벽 가이드(대충 이런 말이었음)-
나는 비밀번호를 사람들이 유추할 수 없이 정말로 어렵게 (아무 뜻이 없는 영문 + 숫자 + 특수문자) 형태로 쓴다면 걱정할 필요가 없겠지만 보통 사용자들은 상징적인 단어(이름, 생일, 어떤 단어 등등..)를 많이 사용하기 때문에 공격자가 조금이나마 이러한 단어들을 조합해서 무차별젹인 대입을 진행한다면 생각보다 쉽게 암호문을 얻을 수 있습니다.
개발자들이라면 이러한 상황을 당연히 대비를 해야 합니다!!
위의 공격법들을 알았으니 이제 보완을 해봐야겠죠?
방법을 알아보기 이전에 명심해야 할 점은 이 보완법들은 공격을 원천차단하는 기술이 아닙니다. 그저 공격 난이도를 급격하게 상승시키는 방법일 뿐이죠. 그렇기 때문에 파훼가 될 것 같은데? 에 대한 걱정을 너무 깊게는 하지 않으셔도 됩니다.
해싱은 한 글자만 달라져도 값이 완전히 달라지게 됩니다.
원문 암호문
Apple -> f223faa96f22916294922b171a2696d868fd1f9129302eb41a45b2a2ea2ebbfd
Apple. -> da43ecd7455ab1d611618a965a5249731199857d086168df9cb885038a35a705
그렇다면 암호화 전 평문의 앞이나 뒤에 Random 값을 붙인 뒤 암호화를 하는 방법은 어떨까요? 쓸만해보이죠? 이것이 바로 Salt
기법입니다!
위와 같이 같은 암호일지라도 각각 다른 Salt 문자열을 가지며, 또 이를 추가적으로 암호화를 진행하기 때문에 암호문과 원문을 예측하기는 더더욱 어려워집니다. 이는 같은 원문이라도 Salt 문자열 가짓수에 비례해 수없이 많은 RainbowTable가 필요하다는 말이 되겠죠? (= 단일 RainbowTable을 사용할 수 없게 된다.)
그렇다면 Salt 기법에서 가장 중요한 것은 뭐니뭐니해도 Salt 생성기(난수 생성기)겠죠? 이 생성기에서 뭐가 중요할까요?
인형안에 또 인형이 들어있는 마트료시카
처럼 암호문을 또 암호화를 하는 기법을 말합니다.
이 Key Stretching
기법은 두가지 목적이 있습니다.
원문 - 암호문
간 예측이 어렵게 만들기다음 포스트를 맛보기로 보여드리자면 BCrypt를 통해 2^10
번 2^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);
}
}
}
브루트포스
와 레인보우 테이블
공격에 취약해진다.Salt
와 Key Stretching
기법을 사용한다.Salt
: 평문에 난수를 붙힌 뒤에 암호화를 실시해 평문을 예측하기 어렵게 만드는 방법Key Stretching
: 암호화를 여러번 실시하여 평문 예측과 높은 자원 소모로 브루트포스 공격을 사용하기 난감하게 만드는 방법다음 포스트에서는 Salt
와 Key Stretching
을 모두 적용한 BCrypt
를 살펴보도록 하겠습니다.
유익한 글이었습니다.