IL - Bcrypt & JWT

5513 단어 JWTbcryptJWT

Bcrypt

Bcrypt에 대해 이해하기 위해서는 우선 해쉬 함수에 대해 알아볼 필요가 있다.

해쉬 함수란 임의의 길이의 데이터를 고정된 길이의 데이터로 매핑하는 함수로 원래는 빠른 데이터 검색을 위해 사용되는 함수이다.
해쉬 함수는 해쉬화된 결과값으로는 원래의 값(원본 데이터)을 유추할 수 없다는 특징이 있다(단방향 해쉬 함수).

encrypt와는 다르게 해쉬는 단뱡향이기 때문에 키의 여부와 상관 없이 해싱된 값을 복호화해서 원래의 값을 알 수 없고, 해쉬 함수를 통해 해싱된 값을 다이제스트(digest - 원래의 값을 알 수 없도록 소화되었다고 해서 이렇게 부른다)라고 한다.
이렇게 다이제스트는 원래의 값을 알 수 없도록 뭉게져있기 때문에 매우 안전할 것 같지만 사실은 그렇지 않다. 바로 레인보우 테이블의 존재 때문인데, 레인보우 테이블이란 해쉬 함수들로 만들어 낼 수 있는 값들을 대량으로 저장한 테이블로, 브루트 포스 공격을 통해 사용자의 암호를 알아내거나 다이제스트를 평문으로 해석하기 위해 존재한다. 한번 해싱되어 저장된 정보들은 이러한 레인보우 테이블을 통해 간단하게 뚫릴 가능성이 크기 때문에 우리는 대안을 찾아야 한다.

레인보우 테이블 등에 대항하기 위해(컴퓨팅의 발전으로 인한 브루트 포스의 속도 증가 등도 포함) 우리가 사용할 수 있는 방법은 Salting과 Key-Stretching이다.

Salting

Salting은 말 그대로 소금을 친다는 것으로, 해쉬 함수에 원본 데이터를 넣기 전 임의의 문자열(데이터)를 추가해서 해싱을 한다는 의미이다.
예를 들어 비밀번호가 'asdfasdf'라면 salt 값으로 임의의 문자열('asdflwekfoinw3vjlJdiL') 등을 위에 붙여 'asdfasdfasdflwekfoinw3vjlJdiL'로 만들어 해쉬 함수에 넣어주는 것이다. 그렇게 되면 다이제스트를 가지고 원본 데이터를 찾아낸다고 하더라도 무엇이 중요한 데이터(원본 데이터)이고 무엇이 salt인지 알 수 없어 더욱 안전해지는 것이다. 또한 salt는 임의의 문자열을 사용하므로 레인보우 테이블에 해당 다이제스트(salt를 추가하여 해싱한 다이제스트)가 없을 확률이 더 크기에 원본 데이터를 유추하는 시간이 더욱 길어지게 된다.

Key-Stretching

Salting과 더불어 해쉬 암호화의 단점을 보완하기 위해 만들어진 것으로, 단순하게 해싱하는(해쉬 함수를 통과시키는) 행위를 반복하여 진행하는 것이다. 예를 들어 A라는 데이터를 해싱해 B라는 다이제스트를 얻으면 B를 다시 해싱해 C라는 다이제스트로 만들어 저장하는 것이다. 물론 반복한 횟수를 개발자 본인만 알고 있는 것이 제일 좋겠지만 설령 해싱 함수를 돌린 횟수를 공격자가 알고 있다고 하더라도 최종 다이제스트의 원문 값을 알기 위해 소모되는 시간이 더욱 길어지므로 상당히 곤란해지게 된다.

Salting & Key-Stretching

우리는 우리의 비밀번호를 좀 더 안전하게 저장하기 위해서 위의 방법들을 잘 활용해야 하고, 가장 좋은 방법은 Salting과 Key-Stretching을 반복하는 것이다. Salting하고 해싱한 후 다시 Salting하고 해싱하는 과정을 반복하면서 비밀번호를 안전하게 암호화 할 수 있다.
물론 이러한 방법은 무조건적으로 안전한 것은 아니며 컴퓨팅의 발전(GPU 연산 속도 발전) 등으로 다시 구시대의 암호화 방식들처럼 안전하지 않아질 수 있다.

지금까지 알아본 해쉬 함수의 보안성을 높이기 위한 방법들을 쉽게 적용하고 비밀번호를 쉽게 암호화하기 위해 사용되는 것이 바로 Bcrypt이다.
Bcrypt는 자동으로 임의의 Salt를 생성해주고 Key-Stretching하여 비밀번호를 암호화해주므로 해쉬 함수, Salting, Key-Stretching의 정확한 원리를 모르더라도 암호화를 적용할 수 있게 해준다.

Bcrypt example

그렇다면 Bcrypt를 사용해보자.
우선 설치를 해야하는데

pip install bcrypt

로 간단하게 설치가 가능하다.

test.py라는 파이썬 파일을 열고 테스트해보도록 하겠다.

import bcrpyt

password = '1234'
encoded_password = password.encode('utf-8')
print(encoded_password) # b'1234'

hashed_password = bcrypt.hashpw(encoded_password, bcrypt.gensalt())
print(hashed_password) # b'~~~~~' salt가 랜덤값이라 값은 다 다를것이다.

와 같이 코드를 작성하고 실행해보면 해쉬된(암호화된) 비밀번호를 볼 수 있을 것이다.
여기서 bcrypt.gensalt()는 bcrypt가 랜덤한 Salt 값을 만들어주는 것으로 bcrypt.gensalt()를 프린트해보면 계속해서 다른 값이 생성되는 것을 알 수 있고, 저 함수 대신 우리가 원하는 Salt 값을 입력하면 그 값으로 Salting을 할 수 있기는 하지만 고정된 값이므로 추천하지는 않는다.
이렇게 간단하게 bcrypt.hashpw(평문, bcrypt.gensalt())로 해쉬 암호화를 할 수 있다. 그러나 Bcrypt는 입력값으로 72 bytes character를 사용해야 한다는 제약이 있다.

JWT

(네? JMT요?)

JWT는 JSON Web Token으로 일반적으로 클라이언트와 서버, 서비스와 서비스 사이에 통신을 할 때 인가(Authorization)을 위해 사용되는 토큰이다. URL에서도 사용될 수 있도록 URL-Safe하게 만들어져있으며 주로 Request의 Header에 담겨있다.

구조

JWT는 dot(.)으로 구분되는 세 부분으로 이루어져 있으며 각각 헤더(header), 페이로드(payload), 시그니처(signature)라고 부른다. 각각 필요한 정보를 담아 보관하고 있으며 그 내용은 다음과 같다.

  • Header: 토큰의 타입과 해쉬 알고리즘에 대한 정보가 담겨있다. 'typ'속성에 토큰의 속성(JWT), 'alg' 속성에 해싱에 사용된 알고리즘 정보가 담겨있고, 그 알고리즘으로 Signature와 토큰을 검증한다.
  • Payload: 토큰에 담을 Claim 정보를 포함하고 있으며, Payload에 담는 정보의 한 조각을 Claim이라 부른다. Claim은 key/value pair로 이뤄져 있으며 여러 조각(claim)을 넣을 수 있다.
    Claim에는
    Registered Claim(토큰 정보를 표현하기 위해 이미 정해진 종류의 데이터),
    Public Claim(사용자 정의 Claim으로 공개용 정보를 위해 사용, 충돌방지를 위해 URI 포맷을 사용),
    Private Claim(사용자 정의 Claim으로 서버와 클라이언트 사이에 임의로 지정한 정보를 저장)
    세 종류가 있다.
  • Signature: Secret_Key를 포함하여 암호화되어 있다. 헤더와 페이로드는 암호화를 한 것이 아니라 단순히 JSON문자열을 base64로 인코딩한 것뿐이라 이 값을 다시 디코딩하면 JSON에 어떤 내용이 들어있는지 확인할 수 있다. 토큰을 사용하는 경우 이 토큰을 다른 사람이 위변조할 수 없어야 하므로 데이터가 위변조되었는지를 검증하기 위한 부분이 Signature 부분이다. 헤더와 페이로드 부분을 .으로 이어붙이고 alg, secret_key로 인코딩하여 생성한다.

JWT 활용

JWT는 주로 로그인 상태를 알려주는 역할을 하고, 매 페이지 이동마다 로그인을 할 수 없으니 로그인을 진행한 클라이언트가 토큰을 가지고 있다가 페이지를 이동하면서, 필요한 데이터를 요청하면서 JWT 토큰을 같이 보내 인가(Authorization) 받는 과정을 거친다. JWT를 주고받는 과정은 아래와 같다.

  • 클라이언트가 서버에게 아이디와 비밀번호를 보내는 로그인 요청을 한다.
  • 서버는 아이디와 비밀번호를 데이터베이스와 비교해 일치하면 토큰을 넘겨주고, 실패하면 에러 메시지를 넘긴다.
  • 토큰을 받은 클라이언트는 요청이 필요한 곳마다 서버로 토큰을 보내면 서버에서 유효한지 확인 후 필요한 정보를 반환한다.

이런 과정을 통해 인가를 진행하게 되는데 토큰에는 클레임셋을 암호화하지 않는다는 특징을 가지고 있다. 그러므로 서명 없이도 누구나 원문에 접근할 수 있기에 보안에 중요한 정보들은 담으면 안된다. 즉 user-id와 같은 사용자를 식별하기 위한 보안에 민감하지 않은 최소한의 정보만을 담고 있어야 한다.

좋은 웹페이지 즐겨찾기