사용 목적
- 사용자의 핸드폰 번호나 계좌 번호와 같은 민감한 정보를
데이터 베이스에는 암호화 하여 저장하며
권한이 있는 사용자에게만 복호화 하여 데이터를 제공 한다.
비밀번호
비밀번호 암호화는
장고의 AbstractBaseUser를 상속받아 사용할 수 있는
set_password를 사용하여
PBKDF2 algorithm with a SHA256 hash 알고리즘을 이용하여
비밀번호 암호화
해싱된 데이터이므로 원본 데이터로 복호화 할 수 없다.
암호화 알고리즘
- 비밀번호를 제외한 암호화가 필요한 개인정보는
python AES 암호화 알고리즘 채택
- pycryptodome Library 다양한 암호화 알고리즘중 AES 알고리즘 사용
블록 알고리즘 대칭키 AES를 사용하여 암·복호화 과정 구현
나의 AES 알고리즘 git hub 설명
대칭키 알고리즘
- 암호화, 복호화 하는 과정에서 KEY 값이 필요하다.
- 문을 열고 들어갈때 비밀번호를 기억하고 있는 것처럼
다시 문을 나올때도 똑같은 비밀번호가 필요하다.- KEY값만 있다면 해당하는 key로 암호화된 데이터를
복호화 할 수 있으므로 노출되어선 안되며,
주기적으로 변경도 해 주어야 한다.
install
pip install pycryptodome > pip poetry add pycryptodome > poetry
import
import base64 import os from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from binascii import Error
- base64
바이너리 데이터를 문자 코드에 영향을 받지 않는 공통 ASCII 문자로 표현하기 위해 만들어진 인코딩- os
환경 변수 관리, 노출되어선 안되는 대칭키를 Git Hub에 업로드 하지 않고
로컬에서만 저장하기에 환경 변수를 불러오기 위해 사용- AES
암호화 알고리즘 AES는 암호화 키로 128, 192, 256 bit를 가질 수 있다.
이때 bit수가 높을 수록 보완성이 향상되지만 그만큼 암호화, 복호화 하는 과정에서 많은 데이터 소모가 요구된다.- pad
암호화 과정에서 만약 문자열의 길이가 3이고
암호화 하는 bit 수가 8이라면
3의 문자열 길이를 8이 되게끔 부족한 공간을 채워주는 역할을 한다.- unpad
암호화 과정에서 채워준 빈 공간을 없애주는 기능을 한다.- Error
복호화 과정에서 생긴 오류를 해결하기위해 import 하였다.
만약 복호화 과정에서 암호화가 되지 않은 데이터가 전달됬을때의
예외 처리를 위해 import
class AESAlgorithm:
AES_KEY = os.environ.get('AES_KEY')
AES_KEY = bytes(AES_KEY, 'utf-8')
@classmethod
def encrypt(cls,data):
cipher = AES.new(cls.AES_KEY, AES.MODE_ECB)
cipher_data = cipher.encrypt(pad(data.encode(), AES.block_size))
return base64.b64encode(cipher_data).decode()
이해하기
AES_KEY = os.environ.get('AES_KEY') # 대칭키 환경변수를 이용하여 불러오기
AES_KEY = bytes(AES_KEY, 'utf-8') # byte로 저장된 대칭 키를 utf-8로 적용
@classmethod # 추상 클래스를 이용하여 외부에서 접근시 # 해당 클래스에 대한 오브젝트 생성 없이 접근에 용이하도록 추가
cipher = AES.new(cls.AES_KEY, AES.MODE_ECB) # 암호화 알고리즘 생성 # AES.MODE_ECB는 암호화 알고리즘을 16byte로 지정 # byte는 값이 클 수록 암호화 수준이 높아지지만 # 그만큼 암·복호화 과정에서 많은 성능을 요구한다.
cipher_data = cipher.encrypt(pad(data.encode(), AES.block_size)) # 16byte 블록 사이즈에 맞게 데이터를 패딩 # 원본 데이터의 부족한 데이터의 크기를 패딩으로 매꾸어 준다.
return base64.b64encode(cipher_data).decode() # base64 : 암호화 데이터를 인코딩 # decode : 바이트 문자열을 문자열로 변환
class AESAlgorithm:
AES_KEY = os.environ.get('AES_KEY')
AES_KEY = bytes(AES_KEY, 'utf-8')
@classmethod
def decrypt(cls, cipher_data):
try:
cipher = AES.new(cls.AES_KEY, AES.MODE_ECB)
cipher_data = base64.b64decode(cipher_data)
data = unpad(cipher.decrypt(cipher_data), AES.block_size)
return data.decode()
except (ValueError, Error, ValueError):
return cipher_data
이해하기
cipher = AES.new(cls.AES_KEY, AES.MODE_ECB) # 암호화에 사용된 똑같은 알고리즘의 객체 생성
cipher_data = base64.b64decode(cipher_data) # 암호화된 문자열을 다시 byte 문자열로 변환
data = unpad(cipher.decrypt(cipher_data), AES.block_size) # 암호화 과정에서 매꾸어준 패딩, 블록 사이즈를 없애고 # 기존의 값으로 복원
return data.decode() 복호화한 바이트 문자열을 문자열로 변환하여 반환
- 예외 처리
try: ... except (ValueError, Error, ValueError): return cipher_data
항상 암호화된 데이터만 전송이 된다 보장되는 코드라면
추가할 필요 없는 예외처리
암호화된 데이터가 인자값으로 전달되지 않아 해당하는 에러 발생시
별도의 과정 없이 값을 반환하도록 로직 구현
암호화 예시
데이터1 = 암호화_메서드(데이터1)
데이터2 = 암호화_메서드(데이터2)
데이터3 = 암호화_메서드(데이터3)
복호화 예시
데이터1 = 복호화_메서드(데이터1)
데이터2 = 복호화_메서드(데이터2)
데이터3 = 복호화_메서드(데이터3)
@classmethod
def encrypt_all(cls, **kwargs):
"""
암호화
"""
cipher = AES.new(cls.AES_KEY, AES.MODE_ECB)
basic_data = ['user', 'company_name', 'business_number', 'business_owner_name', 'contact_number', 'company_img', 'orders']
data_dict = {} # key값 특정
for key, value in kwargs.items():
if key in basic_data:
continue
cipher_data = cipher.encrypt(pad(value.encode(), AES.block_size))
encrypt_element = base64.b64encode(cipher_data).decode()
data_dict[key] = encrypt_element
return data_dict
이해하기
- 기존의 데이터를 하나씩 암호화 하는 것과는 암호화 과정은 차이가 없다
다만 패킹, 언패킹을 이용하여 딕셔너리 데이터를 전달 받고
전달 받은 데이터를 반복문을 이용하여 암호화를 거치지 않는 과정이다.
- 암호화 하지 않을 데이터는 건너 뛰기
basic_data = ['user', 'company_name', 'business_number', 'business_owner_name', 'contact_number', 'company_img', 'orders'] data_dict = {} # key값 특정 for key, value in kwargs.items(): if key in basic_data: continue
딕셔너리 데이터에 전달된 데이터들중, 암호화를 하고싶지 않은 데이터가 생길 수 있다.
basic_data 리스트를 선언하고, 요소들로 암호화 하지 않을 필드의 이름을 기재하고, 반복문에서 해당 필드를 마주할시 암호화 과정을 거치지 않고
continue를 이용하여 건너 뛰게 하였다.
@classmethod
def decrypt_all(cls, **kwargs):
cipher = AES.new(cls.AES_KEY, AES.MODE_ECB)
data_dict = {}
for key, value in kwargs.items():
try:
cipher_data = base64.b64decode(value)
data = unpad(cipher.decrypt(cipher_data), AES.block_size)
cipher_element = data.decode()
data_dict[key] = cipher_element
except TypeError:
data_dict[key] = value
except Error:
data_dict[key] = value
except ValueError:
data_dict[key] = value
return data_dict
이해하기
- 마찬가지로 패킹 언패킹을 이용하여 데이터를 전달 받고,
반복문을 통하여 모든 데이터를 복호화 하는 과정을 진행하고 있다.
- 예외 처리
암호화가 되어 있지 않은 데이터가 전달될 경우의 수가 있다.
이때 이미 str 타입의 문자열을 byte타입의 문자열로 변환하고자 할시
type Error가 발생할 수 있으며 이외에도 다른 에러가 발생할 수 있다.
어떠한 에러가 발생할 수 있다는 초점보다는
핵심은 어떤 에러가 발생 하더라도 원본 데이터는 복호화 과정을 거치지 않고
그대로 딕셔너리의 데이터에 반환한다는 점이다.
따라서 위 예외 처리 코드는 다음과 같이 변경해도 무방 하다.except (TypeError,Error,ValueError): data_dict[key] = value # 또는 except: data_dict[key] = value
Serializer의 메서드를 오버라이딩
- create
오브젝트가 생성될때 호출되는 메서드- update
오브젝트가 수정될때 호출되는 메서드- to_representation
데이터가 직렬화 될때 호출되는 메서드
데이터를 암호화는 메서드 만들기
- 오버라이딩이 아닌, 커스텀 메서드를 만들어서
인자값을 전달받아 데이터를 암호화 하고, 암호화된 데이터를
반환하도록 로직을 구현 했다.def encrypt_deliveries_information(self, deliveries, validated_data): """ 오브 젝트 암호화 """ encrypt_result = AESAlgorithm.encrypt_all(**validated_data) deliveries.address = encrypt_result.get('address') deliveries.detail_address = encrypt_result.get('detail_address') deliveries.recipient = encrypt_result.get('recipient') deliveries.postal_code = encrypt_result.get('postal_code') deliveries.save() return deliveries
create 오버라이딩 (암호화 과정)
- 오브젝트가 생성될때 호출되는 메서드
- ModelSerializer의 create메서드를 오버라이딩 했다.
- Super()를 이용하여 부모 클래스의 기능을 이용하여 오브젝트를 생성
- 생성된 오브젝트의 데이터를 추출하고, 데이터를 암호화 한뒤 저장했다.
def create(self, validated_data): """" 배송 정보 오브 젝트 생성 """ deliveries = super().create(validated_data) deliveries = self.encrypt_deliveries_information(deliveries, validated_data) deliveries.save() return deliveries
update 오버라이딩 (암호화 과정)
- 오브젝트가 수정 될 때 호출되는 메서드
- ModelSerializer의 update 메서드를 오버라이딩
- super을 이용하여 부모 클래스의 메서드를 호출하여 오브젝트를 생성
- 새로운 데이터를 암호화 한뒤 저장
def update(self, instance, validated_data): """ 배송 정보 오브 젝트 수정 """ deliveries = super().update(instance, validated_data) deliveries = self.encrypt_deliveries_information(deliveries, validated_data) deliveries.save() return deliveries
to_representation 오버라이딩 (복호화 과정)
- 데이터가 직렬화 되어 가공 될때 호출되는 메서드
- super()를 이용하여 부모클래스의 메서드를 호출하여 오브젝트 생성
- 데이터를 전달하여 복호화를 진행 했다.
def to_representation(self, instance): """ 배송지 모델 데이터 복호화 """ information = super().to_representation(instance) decrypt_result = AESAlgorithm.decrypt_all(**information) return decrypt_result
class DeliverySerializer(serializers.ModelSerializer):
"""
배송 정보 저장 및 업데이트
"""
class Meta:
model = Delivery
exclude = ('user',)
def validate(self, deliveries_data):
"""
우편 번호 검증
"""
validated_result = ValidatedData.validated_deliveries(self.context.get('user'), deliveries_data)
if validated_result is not True:
raise ValidationError(validated_result[1])
return deliveries_data
def encrypt_deliveries_information(self, deliveries, validated_data):
"""
오브 젝트 암호화
"""
encrypt_result = AESAlgorithm.encrypt_all(**validated_data)
deliveries.address = encrypt_result.get('address')
deliveries.detail_address = encrypt_result.get('detail_address')
deliveries.recipient = encrypt_result.get('recipient')
deliveries.postal_code = encrypt_result.get('postal_code')
deliveries.save()
return deliveries
def create(self, validated_data):
""""
배송 정보 오브 젝트 생성
"""
deliveries = super().create(validated_data)
deliveries = self.encrypt_deliveries_information(deliveries, validated_data)
deliveries.save()
return deliveries
def update(self, instance, validated_data):
"""
배송 정보 오브 젝트 수정
"""
deliveries = super().update(instance, validated_data)
deliveries = self.encrypt_deliveries_information(deliveries, validated_data)
deliveries.save()
return deliveries
def to_representation(self, instance):
"""
배송지 모델 데이터 복호화
"""
information = super().to_representation(instance)
decrypt_result = AESAlgorithm.decrypt_all(**information)
return decrypt_result