회사에서 인턴으로 프로젝트를 진행하면서 사용자 계정이 포함된 데이터를 저장할 때 어떻게 저장해야 해킹에 대한 위협에서 안전할까? 고민이 되었던 순간이 있다.
이 때 암호화에 대한 내용을 많이 찾아봤는데, 그 때 공부했던 내용을 정리해서 남겨두고자 한다.
암호화에는 단방향 암호화와 양방향 암호화 두가지 종류가 있다.
각 암호화 방식을 쉽게 설명하면
단방향 암호화는 암호화만 가능하고 복호화가 불가능한 방식의 암호화이고,
양방향 암호화는 암호화한 다음 복호화도 가능한 방식의 암호화이다.
오늘 정리할 내용은 단방향 암호화의 한 종류인 해시 방식이다.
해시 함수는 임의의 길이의 데이터를 고정된 길이의 데이터로 매핑하는 함수로 주로 SHA방식으로 사용된다.
SHA(Secure Hash Algorithm, 안전한 해시 알고리즘)는 서로 관련된 암호학적 해시 함수들의 모음이다. SHA-256, SHA-512 등등 다양한 방식이 있는데 자주 사용되는 방식은 SHA-256방식으로 변환하기를 원하는 문자들을 256bit 길이의 key로 변환해준다.
과정을 간단하게 설명하면 다음과 같다.
SHA-256 과정
(1) 먼저 변환하고자 하는 문자열을 바이너리 형태로 변환합니다. 바이너리 코드는 0과 1로 나타납니다.(2) 바이너리 데이터는 512 비트의 블락들로 나뉩니다. 만약 블락이 512보다 작다면 패딩을 통해 사이즈를 늘려줍니다. 만약 블락이 512보다 크다면 512비트에서 자릅니다.
(3) 앞서 512비트로 나눈 블락들을 다시 32비트로 자릅니다.
(4) 압축 함수를 64회 반복 수행합니다.
(5) 그리고나서 각 블락별로 해시값을 구하는데, 첫번째 블락의 해시 결과가 두번째 블락의 입력값으로 사용되며 두번째 블락의 해시 결과는 첫번째 블락의 해시 결과와 두번째 블락 데이터와 결합해서 구해집니다.
(6) 최종적으로 256비트의 해시값이 생성되는데 이것이 SHA-256 결과값입니다.
이렇게 만든 해시 함수는 다이제스트(digest)라고 부르는데 복호화가 불가능 하기 때문에 비밀번호를 저장할 때 이 방식을 사용해서 암호화한다면 데이터를 가지고 있는 서버측에서도 이 비밀번호가 원래는 어떤 비밀번호였는지 알 수 가 없다.
그렇다면 이 방법은 완전히 안전할까? 아쉽지만 아니다.
해시 함수는 같은 데이터를 암호화 했을때 항상 같은 결과값이 나오기 때문에 많은 시간과 자원이 있다면 직접 대입해 보면서 결과물과 비교해 원래 비밀번호를 알아낼 수 도 있다. 실제로 해커들이 여러 값들을 대입해보면서 얻은 다이제스트들을 모아둔 테이블이 있는데 이것을 레인보우 테이블(Rainbow Table)이라고 한다.
password라는 데이터를 sha256방식으로 해싱하면
5E884898DA28047151D0E56F8DC6292773603D0D6AABBDD62A11EF721D1542D8
라는 데이터가 나오는데 이를 레인보우 테이블 사이트에 검색해보면
이처럼 쉽게 정답 결과물을 찾을 수 있다.
물론 이처럼 단순한 비밀번호가 아닌 이상에야 레인보우 테이블에 등록되어있지 않을 가능성이 높고, 무차별 대입을 통해서 정답을 찾아내기엔 시간적으로 불가능에 가깝긴 하다. 하지만 단순한 비밀번호를 쓴다 하여 쉽게 뚫린다면 그건 그것대로 보안에 문제가 있는 것이니...
그러한 문제를 보완하기 위한 방법으로는 키 스트래칭(Key Stretching)과 솔트(Salt)가 있다.
먼저 키 스트래칭은 간단하게 말해서 해싱을 여러번 수행하는 것이다. 예를 들어 아까 전 처럼 password라는 데이터를 암호화 한다고 하자.
password가 sha-256방식으로 한번 해싱 됐을 떄는 방금 전과 같은
5E884898DA28047151D0E56F8DC6292773603D0D6AABBDD62A11EF721D1542D8
라는 데이터가 나오고, 이것을 한번 더 해싱하면
C9D3DA531D5F49F7570AE4A5B3C2633AED75A0E55A7803B0CB1D4FB86EA4734E
라는 데이터가 나온다.
이런 방식으로 해싱을 n번 진행하여 암호화 하는 기법을 키 스트래칭이라고 부른다.
실제로 방금 전 레인보우 테이블 사이트에서 password를 2번 해싱한 결과물을 검색하면
아까와는 달리 정답을 찾아내지 못하는 모습을 볼 수 있다.
두번째 방법 솔트는 비밀번호 뒤에 임의의 문자열을 덧붙여서 해싱하는 방식이다.
이것도 예를 들어 yoosj97이라는 단어를 솔트로 지정해서 password를 해싱한다고 하자.
그렇다면 passwordyoosj97 이라는 문자열을 해싱하게 되어서
DE07BA9ACB980B67AF1BB357AC639F02A35E76FCCD2394C26A06F673F6804EA0
라는 결과물이 나온다.
이 경우에도 당연히 레인보우 테이블에 결과물이 저장되어있을 가능성은 낮다.
솔트의 경우 솔트값을 유저에 따라서 다르게 설정한다면 한 명의 비밀번호가 유출되더라도 다른 사람의 비밀번호는 안전하게 된다는 장점도 있다.
이 두가지 방법을 같이 쓰는 방법도 있다. 솔트값을 붙여 나온 해싱값에 다시 솔트값을 붙여서 해싱을 하는 것을 n번 반복하는 식이다.
예를 들면
passowrdyoosj97을 해싱한 값 DE07BA9ACB980B67AF1BB357AC639F02A35E76FCCD2394C26A06F673F6804EA0에
다시 솔트를 붙여
DE07BA9ACB980B67AF1BB357AC639F02A35E76FCCD2394C26A06F673F6804EA0yoosj97
해싱을 하면
E79203F7F905A7CE40AC69FA777B4B9CCBF64C1B588EE6385A48C72311BF542A
이런 식으로 결과물이 나오고, 여기에 다시 솔트값을 붙여 해싱하는 것을 n번 반복하는 식이다.
이 경우 사실상 레인보우 테이블에 의해 뚫릴 가능성은 0에 가깝다고 생각한다.
처음 이 부분을 공부할 때 가장 헷갈렸던 부분이다. 해싱은 프론트와 서버 어느쪽에서 진행하는게 맞을까?
결론부터 말하자면 해싱은 서버에서 진행되는게 바람직하다. 만약 해싱한 값을 해커에게 탈취당한다고 가정하자, 해싱을 클라이언트에서 진행한다면 해커는 그 해싱된 값이 원래 어떤 것이었는지 알아낼 필요도 없이 그냥 그 해싱된 값을 통해 서버에 접속하면 그만이다. 하지만 서버에서 진행할 경우, 인증을 진행할 때 입력받은 값을 한번 더 해싱하여 확인하기 때문에 이미 해싱된 패스워드는 쓸 곳이 없다.
따라서 클라이언트에서 해싱을 하는지 여부는 상관이 없지만 항상 서버에서는 해싱 작업을 해주는 것이 좋다는 것이다.
https://ko.wikipedia.org/wiki/%ED%95%B4%EC%8B%9C_%ED%95%A8%EC%88%98
https://ko.wikipedia.org/wiki/SHA