암호화 및 토큰 발행 기초

jomminii_before·2020년 2월 13일
4

오늘은 암호화 및 토큰 발행에 대한 기초적인 부분을 알아보고자 합니다.

암호화란

일단 우린 암호화를 왜 해야할까요? 답은 간단합니다. 내가 아닌 다른 사람이 내 정보를 갈취할 수 없도록 보호하기 위함입니다. 그리고 지금은 법적으로도 개인정보를 암호화해서 관리하도록 하고 있고요.

그러므로 사람들을 대상으로 하는 서비스를 운영하고 있다면 정보의 암호화는 필수라고 할 수 있습니다.


단방향과 양방향 알고리즘

암호화에는 단방향과 양방향, 크게 두 가지 종류가 있습니다. 간단히 말하면 단방향은 평문을 암호문으로 암호화만 할 수 있는 단방향성 알고리즘이고, 양방향은 평문을 암호화도 할 수 있고 암호문을 복호화도 할 수 있는 알고리즘 입니다. 복호화가 안되는 특성 때문에 비밀번호를 관리할 때는 단방향 알고리즘을 사용하는게 좋습니다.

복호화가 안된다면 정작 비밀번호를 입력했을 때 그 값이 맞는지 어떻게 확인하나 하는 생각이 들 수 있는데, 비밀번호 자체를 검증할 때는 입력받은 값을 암호화해서 저장된 암호화된 값과 비교해서 검증을 할 수 있습니다.

이번 글에서 코딩으로 작성해볼 방식도 단방향 알고리즘인데요, 주로 Hash 방식을 통해 구현됩니다.


Hash란

임의의 길이를 갖는 임의의 데이터에 대해 고정된 길이의 데이터로 매핑하는 함수를 Hash 함수라고 하고, 이 함수의 결과물이 Hash 값 입니다. Hash 함수는 그다지 복잡하지 않은 알고리즘으로 구현되어 상대적으로 CPU, 메모리 같은 시스템 자원을 덜 사용하고, 또 같은 입력 값에 대해서는 같은 출력값이 보장되어 위에서 말한 비밀번호 자체 검증을 가능하게 합니다.

그리고 Hash 함수 특성상 원래의 문장을 복호화할 수 없게 완전히 뭉개버리기 때문에 다시 원본을 복원하는게 불가능합니다. 이러한 특성 때문에 보안에 Hash 값이 많이쓰이는데, 아예 문제가 없진 않습니다. 현재까지 개발된 거의 모든 함수는 해시 충돌문제가 확인된 상태고, 문제가 아직 확인되지 않은 표준은 SHA-3가 유일하다고 합니다.

  • 유명한 알고리즘으로 Message-Digest Algorithm(MD)과 Secure Hash Algorithm(SHA) 등이 있습니다.

bcrypt(비크립트)

지금 사용해 볼 bcrypt는 현재까지 사용되는 가장 강력한 해시 메커니즘입니다. 자세한 설명은 아래 글에서 확인해보세요!

비크립트 비교방법

  • 동일한 솔트를 써야 비교가 가능한데, 반환되는 해시값에 솔트를 추가해놓음으로써 이를 가능하게 함. 솔트를 추가함으로써 연산 자체를 더 불가능하게 하는데 의미가 있음.

bcript로 비밀번호 암호화 하기

이번 작업은 python 환경에서 진행합니다. 실습을 위해 새로운python 가상환경을 생성해주세요.
*가상환경 생성이 어려우신 분은 아래 글을 참조해주세요!


가상환경이 생성되었으면, 이제 가상환경에 들어가서 bcrypt를 설치하고 파이썬을 실행합니다. 파이썬을 실행할 때 저 같은 경우는 python만 치면 python2가 실행이 되서 python3를 입력했습니다.

$ conda activate crypt           # crypt라는 가상환경에 진입

(crypt) $ pip install bcrypt     # bcrypt 설치

Successfully installed bcrypt-3.1.7 cffi-1.14.0 pycparser-2.19 six-1.14.0

(crypt) $ python3                # python3 실행

이제 시작해봅시다! bcrypt를 임포트 해주고, 테스트로 사용할 비밀번호를 선언해줍니다. 그리고 비밀번호를 Hash화 해주는 메서드를 실행해줍니다. 이 메서드에서 bcrypt.gensalt()는 해쉬의 복잡도를 결정해주는 요소이고 디폴트 값은 12로 잡혀있습니다. 지금은 아무것도 넣지 않아쓰니 12의 복잡도를 지니겠죠!

bcrypt.hashpw(password, bcrypt.gensalt())

$ import bcrypt

$ password = '12345678'

$ bcrypt.hashpw(password, bcrypt.gensalt())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/LeeJongMin/conda/envs/crypt/lib/python3.8/site-packages/bcrypt/__init__.py", line 61, in hashpw
    raise TypeError("Unicode-objects must be encoded before hashing")
TypeError: Unicode-objects must be encoded before hashing

근데 뭔가 에러가 났습니다. 잘 풀리는 것 같더니 무슨 일 일까요?

문제는 비밀번호의 타입이었습니다. 에러 코드를 잘 읽어보면 유니코드 객체는 hashing 되기 전에 인코딩 되어야한다고 되어있습니다. 지금 상태의 비밀번호는 12345678로 문자열입니다. 하지만 bcrypt로 hashing 하기 위해서는 bytes 타입으로 인코딩을 해줘야합니다.

인코딩을 해서 다시 실행 해보겠습니다.

$ a = password.encode('utf-8')   #  password 를 `bytes` 타입으로 인코딩 

$ a
b'12345678'                      # `bytes` 타입으로 인코딩 된 비밀번호

# 인코딩 된 비밀번호로 암호화
$ b = bcrypt.hashpw(a,bcrypt.gensalt())
b'$2b$12$ERVlEDmzU.6E7J1bS8Jvr.cZpvEU3EeckW7KMS/rC3Sv6n2p2aLla'


이렇게 비밀번호를 bcrypt로 암호화 했습니다. 이제 이 값을 저장해두어야 하는데, bytes 타입으로는 저장을 못하기 때문에 스트링으로 다시 디코딩해서 저장해야합니다. (Http 통신에서도 bytes 타입을 못받기 때문이기도 합니다. json으로 통신.)

$ b.decode('utf-8') # 암호화된 비밀번호를 문자열로 디코딩
'$2b$12$8Zc91EZ2VlZjNzU0tPw34ujrWwutG5fGcDjFT8Nl9up3w1IDmma7.'

이제 사용자가 비밀번호를 입력한다고 가정하고, 방금 암호화해서 저장해놓은 비밀번호와 맞는지 확인해보겠습니다. 사용자가 입력한 암호와 암호화된 암호 모두 bytes로 인코딩 된 상태여야 비교가 가능하다는 점도 참고하세요!

$ c = '12345678'    # 사용자가 입력한 비밀번호

$ bcrypt.checkpw(c.encode('utf-8'),b) # bytes로 인코딩한 c와 암호화된 b와 비교
True                # 비밀번호가 일치

d = "12345666"      # 사용자가 잘못 입력한 비밀번호
$ bcrypt.checkpw(d.encode('utf-8'),b)
False               # 비밀번호가 불일치

이렇게 사용자가 비밀번호를 잘 입력했는지 확인을 완료했습니다. 실제라고 치면 사용자는 로그인에 성공했다고 할 수 있습니다.

그런데 한 가지 궁금한 점이 생길 수 있습니다. 로그인한 사용자는 어떻게 계속 로그인 상태를 유지하면서 권한이 필요한 우리 서비스를 돌아다니는걸까요?

여기에는 토큰이라는 비밀이 숨어있습니다.


JWT

JWT는 JSON Web Token의 약자로 전자서명 된 URL-safe (URL로 이용할 수 있는 문자로만 구성된)의 JSON 입니다.

JWT는 서버와 클라이언트 간 정보를 주고 받을 때 Http 리퀘스트 헤더에 JSON 토큰을 넣은 후 서버는 별도의 인증 과정없이 헤더에 포함되어 있는 JWT 정보를 통해 인증합니다. 이 인증 과정을 통해 사용자는 로그인 상태로 우리 서비스를 돌아다닐 수 있습니다.


JWT로 토큰 생성하기

이제 토큰(token)을 생성해보겠습니다.

먼저 가상환경에 JWT를 설치합니다. 저는 pip에 있는 pyjwt를 설치했습니다.

$ pip install pyjwt          # pyjwt 설치

Installing collected packages: pyjwt
Successfully installed pyjwt-1.7.1

이제 파이썬에서 jwt를 임포트해서 토큰을 발행해보겠습니다.
토큰에는 [유저 정보]와 [secret key], [적용하고자 하는 알고리즘]을 넣어서 발행합니다. 아래는 유저 아이디가 [1]이고, 시크릿키는 [secretkey]이며 [HS256] 알고리즘을 사용해 토큰을 발행하겠다는 명령어 입니다.

import jwt

$ jwt.encode({<유저정보>}, <시크릿키>, algorithm = '특정 알고리즘')   # 명령어 구성

$ token = jwt.encode({'user_id': 1}, 'secretkey', algorithm = 'HS256')   # 실제 명령어 작성

$ token                     # 발행된 토큰
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.B9XWphE_QU5UZAT8jhKg_jYNGmZOfSL1gcl-3kZeuoI'

이제 발행된 토큰을 프론트엔드 개발자에게 사용할 수 있도록 넘겨주면 되는데요, 이 토큰도 현재 타입이 bytes로 되어있기 때문에 스트링 형태로 바꾸어 전달해줘야 합니다.

$ token.decode('utf-8')    # 문자열로 디코딩
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.B9XWphE_QU5UZAT8jhKg_jYNGmZOfSL1gcl-3kZeuoI'

그리고 bcrpyt와 다른 점은 토큰을 확인할 때, 토큰을 jwt로 디코딩하면 기존에 입력했던 유저정보가 반환됩니다.

$ jwt.decode(token,'secretkey',algorithm = 'HS256')
{'user_id': 1}             # 기존 유저정보 반환

그렇기 때문에 디코딩할 수 있는 정보에 대한 보안이 중요한데요, Django의 경우 secret key가 내부 settings.py에 기본적으로 작성되도록 되어있는데 이렇게 하면 보안에 좋지 않습니다. 실제 프로젝트 진행 시 따로 파일을 만들어 관리해야 좀 더 보안성을 높일 수 있습니다.

지금까지 암호화 및 토큰 발행에 대한 기초를 알아봤습니다. 이후 지식이 디벨롭되면 추가 시리즈로 글을 보완하도록 하겠습니다!

참고자료

profile
https://velog.io/@jomminii 로 이동했습니다.

1개의 댓글

comment-user-thumbnail
2021년 2월 2일

너무 정리 잘 해주셔서 감사해요! 잘 읽고 갑니다!

답글 달기