개인 정보 유출 사고가 잇따르면서 패스워드 암호화 방법에 대한 고민이 깊어지고 있다. 과거에는 MD5, SHA1, SHA-256 해시를 사용하거나, 일부 기업이 여기에 salt를 추가하는 방식을 택했다. 그러나 SHA1 + salt도 이제는 안전하다고 보기 어렵다. 그 근거를 아래에 정리한다.
SHA1과 MD5가 왜 위험한가?
MD5, SHA1, SHA-256 알고리즘이 보안에 취약하다는 것은 간단한 테스트로 확인할 수 있다. "http://google.com"을 MD5로 인코딩한 값(c7b920f57e553df2bb68272f61570210)을 md5.rednoize.com에 입력하면 즉시 원문으로 복호화된다.
이 사이트는 원본 데이터와 해시값 쌍을 데이터베이스로 보유하고, 등록된 해시값을 즉시 원문으로 변환하는 구조다. 딕셔너리가 충분히 확보되면 패스워드를 쉽게 해독할 수 있다는 뜻이기도 하다.
이처럼 “해시값이 이것이면, 암호는 이것”이라는 Rainbow Table을 사용해 일방향 암호화를 무력화한다. MD5, SHA1, SHA-256은 이미 한물간 패스워드 해싱 알고리즘이다.
SHA1 + salt도 안전하지 않은 이유
Rainbow Table 공격에 대한 보완책으로 등장한 것이 SHA1 + salt다. salt를 추가하면 Rainbow Table 방식을 실질적으로 무력화할 수 있다고 여겨왔다. 처리 방식은 아래와 같다.
$salt = "this is a salt";
$password = 'this is an password';
$hash = sha1($salt.$password);그러나 이 방법도 GPU 병렬 처리를 활용하면 초당 수억 건의 해시 연산이 가능해 무력화될 수 있다. 딕셔너리 공격과 GPU 브루트 포스를 결합하면 대형 사이트에서도 상당한 양의 패스워드를 짧은 시간 안에 해독할 수 있다.
현재 권장되는 강화 방법
- 사용자별로 고유한 salt 값과 반복 횟수를 각각 부여한다.
- PBKDF2, Bcrypt, HMAC 같은 강력한 키 도출 함수를 사용한다.
- 사용자에게 충분한 강도의 패스워드를 사용하도록 유도한다.
PBKDF2 구현 예제 (Java)
public class PBKDF2 {
// 임의 salt를 생성
private static byte[] createSalt() throws NoSuchAlgorithmException {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
byte[] salt = new byte[32];
random.nextBytes(salt);
return salt;
}
private static byte[] pbkdf2(char[] password, byte[] salt)
throws InvalidKeySpecException, NoSuchAlgorithmException {
SecretKeyFactory sf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
// 반복 횟수: 10000번, 결과 길이: 256bit
KeySpec ks = new PBEKeySpec(password, salt, 10000, 256);
SecretKey sk = sf.generateSecret(ks);
return sk.getEncoded();
}
private static void logging(String format, Object ... args) {
System.out.printf(format + "%n", args);
}
public static void main(String[] args)
throws NoSuchAlgorithmException, InvalidKeySpecException {
String p1 = args[0];
String p2 = args[1];
logging("password 1= %s", p1);
logging("password 2= %s", p2);
byte[] salt = createSalt();
logging("salt: %s", Arrays.toString(salt));
byte[] d1 = pbkdf2(p1.toCharArray(), salt);
byte[] d2 = pbkdf2(p2.toCharArray(), salt);
logging("derived 1= %s", Arrays.toString(d1));
logging("derived 2= %s", Arrays.toString(d2));
}
}코드에서 10000이 반복 횟수다. 이 값을 높일수록 해독 난이도가 올라간다. 사용자별 반복 횟수를 데이터베이스에 저장해 두면 나중에 값을 높여 안전성을 단계적으로 향상시킬 수도 있다.
마치며
MD5나 SHA1 단독 해싱은 물론, SHA1 + salt 조합도 GPU 연산 능력이 발전한 현재 환경에서는 충분한 보호 수단이 되지 못한다. 패스워드 해싱에는 계산 비용을 의도적으로 높인 PBKDF2, Bcrypt, scrypt 같은 알고리즘을 사용하는 것이 현재 업계 표준이다. 반복 횟수나 비용 파라미터를 조정해 하드웨어 발전에 맞춰 안전성을 지속적으로 높여 나가는 것이 중요하다.

