Django 데이터 암·복호화

손성수·2023년 7월 5일
0
post-thumbnail

사용 목적

  • 사용자의 핸드폰 번호나 계좌 번호와 같은 민감한 정보를
    데이터 베이스에는 암호화 하여 저장하며
    권한이 있는 사용자에게만 복호화 하여 데이터를 제공 한다.

비밀번호

비밀번호 암호화는
장고의 AbstractBaseUser를 상속받아 사용할 수 있는
set_password를 사용하여
PBKDF2 algorithm with a SHA256 hash 알고리즘을 이용하여
비밀번호 암호화
해싱된 데이터이므로 원본 데이터로 복호화 할 수 없다.

암호화 알고리즘

  • 비밀번호를 제외한 암호화가 필요한 개인정보는
    python AES 암호화 알고리즘 채택

  • pycryptodome Library 다양한 암호화 알고리즘중 AES 알고리즘 사용
    블록 알고리즘 대칭키 AES를 사용하여 암·복호화 과정 구현

나의 AES 알고리즘 git hub 설명

AES 알고리즘 Git Hub Url

대칭키 알고리즘

  • 암호화, 복호화 하는 과정에서 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

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
profile
더 노력하겠습니다

0개의 댓글