Java

Hash에 Salt 치기

체리필터 2021. 3. 11. 14:07
728x90
반응형

Hash란 무엇인가? 영어 단어를 찾아 보면 "고기와 감자를 잘게 다져 섞어 요리하여 따뜻하게 차려 낸 것" 이라고 나온다.

무엇인가를 잘게 잘라내는 것을 말하는 것으로 보인다. 우리가 프로그램을 개발하면서 이 단어를 볼 수 있는 곳은 HashMap 에서 주로 볼 것이다.

또한 암호화와 관련되어서 Hash를 하는 경우도 있다.

무엇이 되었던 어떠한 값을 잘게 쪼개어 겹치지 않도록 나눈다는 뉘앙스를 준다.

오늘 다루고자 하는 것은 특정한 값을 Hash하여 고유한 값을 가지는 특정 문자열로 바꿔주는 것을 다루고자 한다.

우리가 자주 사용하는 Hash에는 MD5, SHA1, SHA256, 512 등이 있다.

MD5를 예를 들어 사용해 보면 아래와 같이 나오게 된다. ( www.baeldung.com/java-md5 참고)

    @Test
    public void MD5Test() throws NoSuchAlgorithmException {
        String hash = "35454B055CC325EA1AF2126E27707052";
        String password = "ILoveJava";

        MessageDigest md = MessageDigest.getInstance("MD5");
        md.update(password.getBytes());
        byte[] digest = md.digest();
        String myHash = DatatypeConverter
                .printHexBinary(digest).toUpperCase();

        System.out.println(myHash);

        Assert.assertTrue(myHash.equals(hash));
    }

"ILoveJava"란 String을 MD5로 Hashing을 하게 되면 "35454B055CC325EA1AF2126E27707052"란 문자가 나오게 되는 것이다.

"35454B055CC325EA1AF2126E27707052"란 문자를 보고 "ILoveJava"란 문자열을 알아내기란 쉽지 않기 때문에 이러한 Hash 함수를 사용하여 사용자의 비밀번호를 암호화해서 DB에 저장해 두게 된다.

이렇게 할 경우 Hash의 특성 상 디코딩이 안되기 때문에 관리자라 하더라도 인코딩 되어 저장된 값을 보고 사용자의 비밀번호를 알 수 없게 된다.

하지만 해쉬의 위험성은 바로 "단어사전 입력 공격"과 "무차별 대입공격"이다.

왼쪽은 단어사전 입력공격, 오른쪽은 무차별 대입 공격이다.

즉 인코딩 된 String을 보고서 원문이 무엇인지 알기는 힘들겠지만 단어사전에 있는 특정 단어를 이용하거나 무차별적으로 문자를 생성해서 해슁을 한 후 값이 일치하면 해당 해슁된 값의 원문이 무엇인지 알아내는 방법이다.

더 나아가 이미 해슁된 Lookup Table을 만들어 놓고 비교해 가며 해슁되기 전의 원문을 알아내고 있다.

해슁된 값을 통해 원문을 조회할 수 있다.

이러한 단어사전 입력공격이나 무차별 대입공격을 막을 방법은 없다.

따라서 이러한 위험성을 피하기 위해서 우리는 소금치기(Adding Salt)를 할 필요가 있다.

원문은 같아도 소금으로 인해 해슁된 값은 달라진다.

해슁의 단점은 원문과 해슁된 문자와 1:1 관계이기 때문에 나와 동일한 비밀번호를 쓰는 사람의 경우 해슁된 문자도 동일하다고 유추해 볼 수 있다.

이러한 위험성을 방지하기 위해 무작위로 만들어진 문자열을 원문에 붙인 후 Hash 하게 되는 것이다.

소금 값을 사용할 때 주의 해야 할 점은 너무 짧은 소금값을 사용하거나, 하나의 소금값으로 여러 군데서 사용하는 경우이다.

또는 잘못된 관행으로 이중 해쉬를 한다거나 자신만의 해쉬를 만들어 사용하는 경우가 있을 수 있다. 아래와 같은 방법은 사용하지 말아야 한다.

  • md5(sha1(password))
  • md5(md5(salt) + md5(password))
  • sha1(sha1(password))
  • sha1(str_rot13(password + salt))
  • md5(sha1(md5(md5(password) + sha1(password)) + md5(password)))

현재 나온 것 중에 가장 안전한 단방향 해쉬 함수는 BCrypt 이다. 이와 관련한 내용은 d2.naver.com/helloworld/318732 를 참고해 볼 수 있다.

springboot를 기준으로 BCrypt를 분석해 보면 Salt를 어떻게 사용하는지 알 수 있다.

일단 아래와 같이 테스트를 돌려 보자.

    @Test
    public void BcryptTest() {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();

        String plainText = "abcd";
        String encryptedText = bCryptPasswordEncoder.encode(plainText);

        boolean result = BCrypt.checkpw(plainText, encryptedText);
        Assert.assertTrue(result);

        System.out.println(plainText);
        System.out.println(encryptedText);
    }

위 테스트 메소드를 실행하게 되면 plainText와 encryptedText를 화면에 찍고 있는데 매번 실행할 때 마다 plainText는 동일하지만 encryptedText 값은 다르게 나오는 것을 볼 수 있다.

abcd
$2a$10$aQuVnGKh.ifc.9Dv4hoUUetKtEfwdcMG.D1EI8xymcFZXfWEqDxA6

abcd
$2a$10$/dmsulIDT0FX8WBf0YT7VuNXeybvaXObuWesC2.meHkhbLvagYSIG

abcd
$2a$10$LjeHevvQM3fokaQmf..cPOwmzgp/hdyosEEUlhbQQs.P8zeZQhfry

이것은 바로 spring이 구현해 놓은 BCrypt Encoder에서 자동으로 salt를 생성해서 원문에 붙여 주고 있기 때문이다.

조금 더 깊숙하게 들어가 보면 다음과 같은 내용을 볼 수 있다.

org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder.encode() 메소드를 보면 아래와 같다.

	@Override
	public String encode(CharSequence rawPassword) {
		if (rawPassword == null) {
			throw new IllegalArgumentException("rawPassword cannot be null");
		}
		String salt = getSalt();
		return BCrypt.hashpw(rawPassword.toString(), salt);
	}

getSalt()라는 메소드를 통해 소금값을 랜덤으로 생성한 후 hash 하는 곳에서 사용하고 있다. getSalt 메소드 안에 들어가 보면 BCrypt.gensalt 라는 정적 메소드를 호출하고 있으며, 이 곳에서는 SecureRandom 을 사용하여 소금값을 만들어 내고 있다.

그리고 BCrypt.hashpw 메소드 안에서는 다음과 같이 salt를 이용하여 hash 하고 있다.

hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 0);

그리고 나서 StringBuilder 타입의 변수인 rs에 다음과 같이 담고 있다.

encode_base64(saltb, saltb.length, rs);
encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1, rs);

따라서 우리가 위에서 돌려본 테스트 코드에서 나온 결과값은 "salt + hash" 값이며 checkpw 에서는 결과 값중에 진짜 salt 값을 분리해 내어 hash 해 보고 비교한 다음 입력된 비밀번호가 유효한 값이지 확인한다.

real_salt = salt.substring(off + 3, off + 25);
saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);

 

 

crackstation.net/hashing-security.htm

 

Secure Salted Password Hashing - How to do it Properly

Salted Password Hashing - Doing it Right If you're a web developer, you've probably had to make a user account system. The most important aspect of a user account system is how user passwords are protected. User account databases are hacked frequently, so

crackstation.net

d2.naver.com/helloworld/318732

 

728x90
반응형