Java에서의 암호화(SHA-256, SHA-512)

기르기르·2022년 11월 14일
1
post-custom-banner

해시라는 건 무엇일까?

해시는 가변길이의 데이터를 고정된 길이의 데이터로 변환시키는 것을 말한다. 자바 Collection Framwork의 포스팅에서도 기재한대로 이 해시 또한 해시 값 자체를 index로 사용하고 키-값을 사용하여 데이터를 저장한다.
대표적인 해시 알고리즘으로 MD5(Message Digest)와 SHA가 있는데 JAVA에서 이를 사용하려면 MessageDigest클래스가 필요하다.

MessageDigest

JAVA에서 MessageDigest 클래스는 단방향 해시 함수 값을 구할 때 사용한다.
더 자세히 예를 들어 다음과 같은 상황에서 사용한다.

  • 비밀번호 및 특정 정보를 암호화하여 저장하고 싶을 때
  • 파일의 유효성을 해시 값을 통해 확인하고 싶을 때

MessageDigest의 메소드

(1) getInstance() : MD5, SHA-256, SHA512를 대입할 경우 대입한 메시지 다이제스트 오브젝트를 작성한다.
파라미터로 받는 알고리즘은 NoSuchAlgorithmException 때문에 try / catch로 감싸줘야 한다.
(2) update() : 데이터를 해시(다이제스트를 갱신)한다
(3) digest() : 바이트 배열로 해시를 반환하고 적은 데이터일 경우 digest에 직접 입력 가능하다.

SHA 알고리즘이라는 것은?

SHA(Secure Hash Algorithm)는 미국 국가 안전 보장국(NSA)에서 개발된 함수로 SHA는 해시값을 이용한 암호화 방식이며 단방향 알고리즘이다.
최초에 SHA-0가 만들어지고 그 후 변형하여 SHA-1, SHA-2가 만들어졌다.
SHA-0과 SHA-1는 최대 160bit의 고정길이로 요약하고 SHA-2는 사용 함수의 뒤 숫자 만큼 가능하며 SHA-2가 오늘 설명하는 SHA-224, SHA-256, SHA-384, SHA-512 등을 묶어서 부르는 명칭이다.
복호화가 불가능한 단방향 알고리즘을 사용하는 이유는 제대로 설계된 웹 서비스라면 개발자와 운영자가 개개인의 Password를 알 수 없어야하기 때문이다.
어째서 복호화가 불가능한지 알아보기 위해서는 실제 SHA-256으로 인코딩하여 나오는 값을 보는 것이 빠르다.
사용자의 패스워드 hunter2를 SHA-256의 인코딩을 이용해 다이제스트 값을 얻으면
f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7
이지만 한 글자 차이인 hunter3를 인코딩 하였을 때에는
fb8c2e2b85ca81eb4350199faddd983cb26af3064614e737ea9f479621cfa57a
의 값을 얻게 된다.
이렇게 조금의 차이로도 값이 크게 달라져 유추하는 것도 불가능하게 만들어진 것이 SHA 알고리즘이다.

SHA-256

SHA 알고리즘의 한 종류로서 256비트로 구성되며 64자리 문자열을 반환한다. 해시 알고리즘 SHA-2 계열 중 하나이며 2^256만큼의 경우의 수를 만들 수 있다.
SHA-256은 어떤 길이의 값을 입력하더라도 256비트의 고정된 결과 값을 반환한다. 입력 값이 조금만 변동되더라도 반환값이 완전히 달라지기 때문에 반환값을 토대로 입력값을 유추하는 것이 거의 불가능하다.
복호화를 하지 않는 SHA의 특성상 비밀번호 일치여부를 확인하기 위해서는 다시 SHA-256을 이용한다.
이것이 가능한 이유는 동일한 메시지는 항상 동일한 다이제스트 값을 갖기때문이다. 해커들은 이를 악용해 레인보우 테이블이라는 것을 사용하여 정보를 알아내기때문에 SALT라는 바이트 단위의 임의의 문자열을 함께 사용한다.

	// SHA-256 암호화
	// 1. 입력 값을 256비트(32바이트) 암호화 처리하는 알고리즘
	// 2. 암호화는 가능하지만 복호화는 불가능
	// 3. 1바이트를 2글자로 표현하면 총 64글자(DB에 저장될 때 크기) 공간이 필요
	// 4. 모든 입력이 64글자 암호화 처리
	// 5. java.security 패키지
    
	public String sha256(String pw) {
		MessageDigest md = null;
		try {
			md = MessageDigest.getInstance("SHA-256");
			md.update(pw.getBytes());
		} catch(Exception e) {
			e.printStackTrace();
		}
		byte[] pwd = md.digest();  // 배열 pwd : 문자열 str이 암호화된 32바이트 크기의 배열
		StringBuilder sb = new StringBuilder();
		for(int i = 0; i < pwd.length; i++) {
			sb.append(String.format("%2X", pwd[i])); // %2X(2자리 16진수(0~F))
		}
		return sb.toString();
	}

                                                     [SHA-256의 예시]

레인보우 테이블이라고?

해시 함수(MD5, SHA-1, SHA-2 등)을 사용하여 만들어낼 수 있는 값들을 대량으로 저장한 표이다. 하나의 패스워드에서 시작해 변이된 형태의 여러 패스워드를 생성하여 그 패스워드의 해시를 고리처럼 연결해 일정 수의 패스워드와 해시로 이루어진 테이블이며 몇 천개의 테이블이 합쳐져 최종 테이블이 생성된다. 조합 가능한 모든 문자열을 하나씩 대입해 보는 방식으로 문제를 푸는 브루트 포스 공격을 뒷받침 해주는 역할을 하기도 하고 해시에서 평문을 추출해내기 위함의 역할도 있다.
MD5의 경우 이미 인터넷 상에 수백억 개의 해시 값에 대한 레인보우 테이블이 있어 이를 이용하면 90% 정도의 사용자 패스워드가 크랙 가능하다. 이게 가능한 이유는 미리 가능한 패스워드 조합을 다 계산한 테이블을 가지고 비교 수행하는 사전 공격 형태이기때문이다.

Salt는 소금은 아니구!

Salt는 레인보우 테이블의 약점을 이용한 것이다. 약점이란 레인보우 테이블 새로 생성하여 만들기 위해서는 엄청나게 큰 데이터를 필요로 하기때문에 원본 문자열이 길고 복잡할수록 테이블에 존재할 가능성이 낮아진다는 것이다. 이를 이용하여 무작위 문자열(salt)를 합쳐서 SHA 알고리즘을 적용한다. 이것을 Salting이라고 한다.
가장 효과적인 방법으로는 각 사용자 별 고유의 Salt를 가져야하며 32비트 이상의 길이를 가져야 Salt와 다이제스트를 추측하기 어렵다고한다. 그렇기에 암호학적으로 안전한 난수 생성기를 사용하여 예측 가능성을 줄여야 한다.

                                                   [Salting 값 변환 예시]


    
	public String getSalt(){
		
        // 보안 이슈로 Random이 아닌 SecureRandom을 사용해 주는것이 좋다.
        SecureRandom secureRandom = new SecureRandom();
        
		byte[] salt = new byte[20];
		secureRandom.nextBytes(salt); // 난수 생성
		
		StringBuilder sb = new StringBuilder();
		for(int i = 0; i < salt.length; i++) {
			sb.append(String.format("%2X", salt[i]));
		}
		return sb.toString();
	}
    
    public String sha256(String pw, String salt) {
		MessageDigest md = null;
		try {
			md = MessageDigest.getInstance("SHA-256");
			md.update((pw + salt).getBytes());
		} catch(Exception e) {
			e.printStackTrace();
		}
		byte[] pwSalt = md.digest();
		StringBuilder sb = new StringBuilder();
		for(int i = 0; i < pwSalt.length; i++) {
			sb.append(String.format("%2X", pwSalt[i])); 
		}
		return sb.toString();
	}
	
    public static void main(String[] args) {
		String pw = "password";
        String salt = getSalt();
        String pwSalt = sha256(pw, salt);
        System.out.println(pwSalt);
	}

                                                       [Salting의 예시]

이렇게 구해진 salt값과 pwSalt값은 테이블에 저장시키고 사용자가 로그인할 때마다 불러와서 대조한다.
대조하는 방식은 생성할때와 같다. 불러낸 salt값과 기입한 pw를 합쳐 해시암호화하고 값이 같은지를 알아보는 형식이다.

세상에는 참 천재가 많구나

이미테이션 게임이라는 영화..를 사실 제대로 보지는 않고 지나가듯 보아서 줄거리는 대충 아는데 암호화라는게 이런 형식인줄은 꿈에도 생각지 못했다. 암호화가 진화하는만큼이나 해커들도 진화하고 계속 엎치락 뒤치락하는 현실이 오히려 영화 같기도하다. 또 느끼는 점은 성능도 성능이지만 대부분의 웹 개발자에게는 사실 보안 쪽이 가장 우선시되는 것인가?이다. 뭐 사실 당연한 부분이기도 하다. 다음 포스팅으로 Spring Security를 할 것인데 이 또한 보안이니ㅎㅎ

참조 Blog, Web

https://coding-sojin2.tistory.com/entry/%ED%95%B4%EC%8B%9C-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-hash-algorithm (해시 알고리즘)
https://brownbears.tistory.com/73 (해시 알고리즘의 이해)
https://cceeun.tistory.com/241 (SHA)
https://gofnrk.tistory.com/79 (SHA와 레인보우 테이블)
https://ko.wikipedia.org/wiki/%EC%86%94%ED%8A%B8_(%EC%95%94%ED%98%B8%ED%95%99)#cite_note-1 (salt)
https://st-lab.tistory.com/100 (salt의 정의)
http://wiki.hash.kr/index.php/%EB%A0%88%EC%9D%B8%EB%B3%B4%EC%9A%B0_%ED%85%8C%EC%9D%B4%EB%B8%94 (레인보우 테이블)
https://a07274.tistory.com/238 (MessageDigest)
https://m.blog.naver.com/puri8467/221429417229 (MessageDigest)
https://d2.naver.com/helloworld/318732 (Hash, Salt)

post-custom-banner

0개의 댓글