-
[Django]Fernet을 사용한 양방향 암호화공부/Django 2023. 4. 7. 00:01
장고로 개발하면서 DB에 저장되는 데이터 중 암호화 처리가 필요한 데이터가 있다. (데이터는 다 소중하고 중요하다.)
비밀번호 같은 경우는 argon2를 사용해서 단방향 암호화를 한 상태여서 복호화가 되지 않는다.
오늘 블로그에 기록 할 부분은 Fernet을 활용한 양방향 암호화와 ORM으로 DB를 조회 할 때 겪었던 오류와 해결 방법에 대해서 설명을 하려고 한다.
2025.09.29 수정!!!!
댓글에서 친절하게 어떤분께서 알려주셔서 블로그 글을 수정합니다. 잘못된 방식을 전달드려 죄송합니다.
Fernet을 쓰는 이유는 동일한 데이터를 암호화한 값이 다르지만, 복호화는 동일하다는게 Fernet의 장점입니다.
Fernet의 암호화 값이 다른 이유는 timestamp, iv가 암호화할때마다 값이 다르게 생성되면서 발생하고 이는 더 안전하게 데이터를 암호화하는 방식이라고 생각합니다.
제가 겪었던 문제는 Fernet으로 암호화하면 같은 데이터도 매번 다른 암호문이 생성되므로, 암호화된 상태에서는 검색이 불가능합니다.
이렇게 되면 검색을 위한 특정 값을 사용할 수 없습니다.
그렇기 때문에 동일한 데이터(값)을 2가지 방법으로 저장하는 것을 이야기할 수 있을거 같습니다.
1. Fernet 암호화
2. 검색을 위한 lookup 해싱
원본: "user@example.com" → Fernet 암호화: "gAAAAABh..." (매번 다름, DB 저장용) → Lookup : "a1b2c3..." (항상 같음, 검색/인덱스용)1번 방식은 데이터를 암호화해서 저장하고 관리하는 것입니다.
2번 방식은 데이터를 검색용으로 사용하는 방법으로 동일한 데이터를 전달한다면 해싱을 통해서 동일한 데이터로 값이 생성이 됩니다.
해당 값을 이용하여 검색으로 사용할 수 있습니다.
댓글에 알려주신분 늦었지만 지금이라도 알게되서 정말 감사합니다 :)
암호화 vs 복호화
암호화: 암호화는 전송하고, 수신하고, 저장하는 정보를 해독할 수 없도록 정보를 비밀 코드로 변환하는 기술적 프로세스입니다.(Dropbox)
복호화: 복호화(decryption) 또는 디코딩(decoding)은 부호화(encoding)된 정보를 부호(code)화되기 전으로 되돌리는 처리 혹은 그 처리 방식을 말한다. (hashnet)
Fernet
장고의 암호화하는 종류가 여러개 있겠지만 처음에 AES 방식으로 했다가 잘 안돼서 Fernet으로 변경 하였다.
1. Fernet을 사용하기 위해서 cryptography 설치
pip install cryptography2. 코드 구현
from cryptography.fernet import Fernet fenet_key = Fernet.generate_key() # 암호화 키 생성 # 암호화/복호화를 위한 Fernet 객체 생성 fernet = Fernet(key) def encrypt(data): # """data를 암호화하는 함수""" # UTF-8 인코딩된 문자열을 바이트 문자열로 변환 daata_bytes = data.encode('utf-8') # 암호화된 바이트 문자열을 base64 인코딩된 문자열로 반환 return fernet.encrypt(data_bytes).decode('utf-8') def decrypt(encrypted_data): # """암호화된 data 복호화하는 함수""" # base64 인코딩된 문자열을 바이트 문자열로 변환 return fernet.decrypt(encrypted_data).decode('utf-8')한부분씩 코드를 보면서 설명이 필요할 하겠습니다.
fernet_key = Fernet.generate_key()Fernet.generate_key() 는 Fernet을 통해서 키를 발급받는 부분입니다. (이 키를 계속 요청하는 경우 복호화에 차질이 생길 수 있습니다.) 그렇기 때문에 저같은 경우 한번 발급받은 키를 환경변수(env)에 저장한 후에 사용하고 있습니다.
fernet = Fernet(key)암호화 복호화를 위한 Fernet 객체를 생성한다.
def encrypt(data): # """data를 암호화하는 함수""" # UTF-8 인코딩된 문자열을 바이트 문자열로 변환 daata_bytes = data.encode('utf-8') # 암호화된 바이트 문자열을 base64 인코딩된 문자열로 반환 return fernet.encrypt(data_bytes).decode('utf-8') def decrypt(encrypted_data): # """암호화된 data 복호화하는 함수""" # base64 인코딩된 문자열을 바이트 문자열로 변환 return fernet.decrypt(encrypted_data).decode('utf-8')변경을 원하는 데이터는 type이 어떻게 될지 모르지만, 암호화(encrypt) 전에 bytes로 변경이 필요하다. 그래서 encode('utf-8')을 사용 하였다. 그 후 암호화 처리와 함께 다시 decode('utf-8')을 사용하여 문자열로 변화 해 주었다.
decrypt 함수는 암호화된 데이터를 가지고 decrypt를 사용하여 복호화 할 수 있다. 이를 활용하면 암호화, 복호화를 할 수 있다.
- 실제로 내가 적용한 코드 -
from cryptography.fernet import Fernet from config.settings.base import env key = bytes(env(KEY),'utf-8') fernet = Fernet(key) def encrypt(data): data_bytes = data.encode('utf-8') return fernet.encrypt(data_bytes).decode('utf-8') def decrypt(data): return fernet.decrypt(data).decode('utf-8')발급 받은 키는 env에 저장하고 가져와서 사용을 하고 있다. 이렇게 하면 내가 원하는 곳에서 암호화, 복호화가 진행이 된다.(admin에서도 잘 보인다. DB 에서 조회하면 암호화 된 데이터 확인이 가능하다. (python mange.py dbshell))
- 특정 필드에 적용 방법 -
from .encrypt_utils import encrypt, decrypt class EncryptedCharField(CharField): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # 암호화 처리 def get_prep_value(self, value): if not value: return None return encrypt(value) # 복호화 처리 def from_db_value(self, value, expression, connection): if value is None: return value return decrypt(value)암호화를 사용하려고 하는 필드에서 사용할 수 있도록 CharField로 만들고 모델에서 필드를 선언 할 때 EncryptedCharField를 사용하면 된다.
그냥 암호화, 복호화만 하실분 들에게는 여기까지 적용하면 될거 같습니다. (암호화했던 필드를 Example.object.filter()를 사용하지 않으시다면.....)
그러나 여기서 문제가 하나 발생한다. 암호화를 시도 했고 복호화도 잘 된다. 내가 만약 name을 암호화 했다면 Example.object.filter(name=name)으로 검색하면 값이 올거라고 생각했는데 오지 않는다.... 발급받은 KEY값을 줘도 안온다......
그 이유는 라이브러리를 뜯어봐야 알 수 있었다.
- 라이브러리에 있는 코드 -
# 라이브러리에서 가져온 코드입니다. 하단에서 한 줄로 정리하겠습니다. def encrypt(self, data: bytes) -> bytes: return self.encrypt_at_time(data, int(time.time())) def encrypt_at_time(self, data: bytes, current_time: int) -> bytes: iv = os.urandom(16) return self._encrypt_from_parts(data, current_time, iv) def _encrypt_from_parts( self, data: bytes, current_time: int, iv: bytes ) -> bytes: utils._check_bytes("data", data) padder = padding.PKCS7(algorithms.AES.block_size).padder() padded_data = padder.update(data) + padder.finalize() encryptor = Cipher( algorithms.AES(self._encryption_key), modes.CBC(iv), ).encryptor() ciphertext = encryptor.update(padded_data) + encryptor.finalize() basic_parts = ( b"\x80" + current_time.to_bytes(length=8, byteorder="big") + iv + ciphertext ) h = HMAC(self._signing_key, hashes.SHA256()) h.update(basic_parts) hmac = h.finalize() return base64.urlsafe_b64encode(basic_parts + hmac)보면 알겠지만 encrypt -> encrypt_at_time -> encrypt_from_parts 로 이어지고 있다.
내가 쓰는 encrypt는 암호화와 복호화에는 문제가 없지만, 암호화 할때마다 값이 달라지는 이유에서는 encrypt_from_parts를 보면 알 수 있었다.( 절대 쉽게 찾지 않음.... 왜그런지 계속 생각하고 내코드의 문제점을 파악해봤지만 결국은 내가 그냥 구현에 급급했다는 생각이 컷다.)
encrypt_from_parts가 받는 parameter를 보면 data, time, iv 3가지가 존재한다. data는 내가 암호화 시킬것 time??? time확인해보니 시간이 찍히고 있었다. 그러니 값이 계속 변경되는 형태.... iv 역시 os.urandom(16)으로 랜덤된 값을 생성하고 있다. time, iv가 암호화 할 때마다 값이 달라지게 하는 원인이였다.
그러나, 이 부분을 고정 값으로 전달한다면 보안적인 측면에서 문제가 생길 수 있다는 점을 항상 생각해야 한다.
암호화 값이 달라지는 이유 요약: encrypt를 사용하면 암호화된 값이 항상 바뀔 수 밖에 없다. 왜냐면 랜덤 값을 parameter로 전달하고 있기 때문!!!!!
-- 고정 값으로 암호화 부분을 수정 한다면 -
import time, os from cryptography.fernet import Fernet from config.settings.base import env key = bytes(env(KEY),'utf-8') # KEY를 bytes로 변경 fernet = Fernet(key) # iv = os.urandom(16) # 랜덤 생성하면 bytes로 값이 생성 but env는 str로 저장해야하기때문에 다시 문자열로 변환필요 # hex = fixed_iv.hex(iv)# 를 사용하면 iv를 문자열로 변환 -> env에 저장 iv = env('IV') fixed_iv = bytes.fromhex(iv) # env에 문자열로 바꿨던 값을 bytes 타입으로 변경하기 위해서 필요 def encrypt(data): data_bytes = data.encode('utf-8') encrypt_data = fernet._encrypt_from_parts(data_bytes, int(env('TIME')), fixed_iv) # int(env('TIME')) -> int(time.time())으로 생성 후 env에 저장 return encrypt_data.decode('utf-8') def decrypt(data): return fernet.decrypt(data).decode('utf-8')이렇게 하면 쿼리 조회나 admin조회가 동일한 값을 넣어주면 가능할 것입니다.
( 홍길동을 찾고 싶다면 '홍길동' full-name으로 넣어줘야 합니다.)
제가 작성한 방법이 정확하고 올바른 방법인지는 확실하지 않지만, 저도 시행착오를 겪고 저와 같은 오류를 겪지 않았으면 해서 작성했습니다. 부족했지만 저보다 좋은 코드를 사용하고 계시다면 저에게 알려주시면 정말 감사하겠습니다.
즐거운 개발 하세요~
-Ref.
https://cryptography.io/en/latest/fernet/
Fernet (symmetric encryption) — Cryptography 41.0.0.dev1 documentation
Encrypts data passed. The result of this encryption is known as a “Fernet token” and has strong privacy and authenticity guarantees. Note The encrypted message contains the current time when it was generated in plaintext, the time a message was created
cryptography.io
Python-Python과-cryptography를-통해-대칭키-암호화-하기
- 어떤 문제를 해결하기위해 검색하고 블로그에 작성한 글입니다. 부족한점이 많지만 틀린점이나 부족한점이 있다면 말씀해주시면 감사하겠습니다.
'공부 > Django' 카테고리의 다른 글
[Django] Custom Middleware를 이용해서 log 작성하기 (0) 2023.04.26 [Django] Logging - 장고 로그(debug_sql) (0) 2023.04.14 Django - CloudWatch : 장고 로깅을 CloudWatch에 입력하기 (0) 2023.04.05 Django - S3 - nginx - 이미지업로드, 정적파일 (0) 2023.03.07 Nginx static 에서 root와 alias 의 차이 (0) 2023.03.04