Published on

Fernet와 AWS secret manager를 활용한 암호화 및 복호화 방법

진행한 프로젝트에서 서드파티API(키와 시크릿)같은 민감한 정보를 유저별로 저장해야 했습니다.

AWS에서 동작하는 PrivateDB 내에 저장하여 사용한다고 하지만, 만에 하나 DB가 노출될 경우 심각한 문제를 초래할 수 있다고 생각했습니다.

특히 해당 부분이 (간접?)금융 관련 데이터&서비스에 접근하는 부분이었기 때문에 API의 경우 더욱 신중하게 다뤄야 한다고 생각했습니다.

이 게시글에서는 Django 애플리케이션에서 API 키와 시크릿을 안전하게 저장하는 방법을 기록합니다.

Django Model 참고사항 with Code

  • salt: 암호화 및 복호화 시 사용되는 임의의 데이터를 저장합니다.
  • exchangeis_issued 는 API 키가 속한 가상화폐 거래소와 거래소 요청 시 오류가 발생했는지를 알기위한 필드이지만, 이번 글에서는 중요하지 않은 부분이므로 설명은 생략하겠습니다.
class ApiKey(BaseModel):
    user = models.ForeignKey("user.User", on_delete=models.CASCADE, related_name="api_keys", db_index=True)
    exchange = models.CharField(
        max_length=24, choices=ExchangeKindChoices.choices, verbose_name="가상화폐 거래소 플랫폼", db_index=True
    )
    api_key = models.CharField(verbose_name="암호화된 api_key", max_length=512, null=True, blank=True)
    api_secret = models.CharField(verbose_name="암호화된 api_secret", max_length=512, null=True, blank=True)
    salt = models.CharField(max_length=255, null=True, blank=True)
    is_issued = models.BooleanField(verbose_name="이상 감지 여부", default=False, help_text="거래소 요청 시 에러 발생")

    class Meta:
        """생략"""

    def __str__(self):
        """생략"""

    @staticmethod
    def combine_secret_and_salt(salt):
        """Secret Manager 에서 Some Secret 값을 가져옵니다. """
        some_secret = get_secret("some_secret").encode()
        salt_with_secret = some_secret + salt
        return salt_with_secret

    @staticmethod
    def generate_key(salt_with_secret):
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt_with_secret,
            iterations=100000,
            backend=default_backend()
        )
        key = kdf.derive(salt_with_secret)
        return base64.urlsafe_b64encode(key)

    def encrypt_keys(self, api_key, api_secret):
        salt = os.urandom(16)
        salt_with_secret = self.combine_secret_and_salt(salt)
        key = self.generate_key(salt_with_secret)
        f = Fernet(key)
        self.api_key = base64.urlsafe_b64encode(f.encrypt(api_key.encode())).decode()
        self.api_secret = base64.urlsafe_b64encode(f.encrypt(api_secret.encode())).decode()
        self.salt = base64.urlsafe_b64encode(salt).decode()

    def decrypt_keys(self):
        salt = base64.urlsafe_b64decode(self.salt)
        salt_with_secret = self.combine_secret_and_salt(salt)
        key = self.generate_key(salt_with_secret)
        f = Fernet(key)
        decrypted_api_key = f.decrypt(base64.urlsafe_b64decode(self.api_key)).decode()
        decrypted_api_secret = f.decrypt(base64.urlsafe_b64decode(self.api_secret)).decode()
        return decrypted_api_key, decrypted_api_secret

1. encrypt

  1. Salt 생성: os.urandom(16)을 사용하여 16바이트 길이의 랜덤 데이터를 생성하고, 이를 salt로 사용합니다.
  2. Some Secret과 결합: combine_secret_and_salt 메서드를 사용하여 생성된 salt 와 Secret Manager에서 가져온 some_secret을 결합합니다.
  3. 키 생성: 결합된 salt_with_some_secret 값을 PBKDF2HMAC 함수에 전달하여 대칭 키를 생성합니다. 이 대칭 키는 데이터를 암호화하고 복호화하는 데 사용됩니다.
  4. 데이터 암호화: Fernet 라이브러리를 사용하여 api_keyapi_secret을 암호화합니다. 암호화된 데이터는 안전하게 저장할 수 있습니다.
  5. Salt 저장: 복호화 시 동일한 키를 생성하기 위해 사용된 salt를 함께 저장합니다.

1.1 Fernet

이 코드에서 사용된 Fernet은 Python의 cryptography 라이브러리에서 제공되는 대칭 키 암호화 도구입니다.

Fernet은 데이터를 암호화하고 복호화하는데 동일한 비밀 키를 사용하는 방식으로, 안전하고 간편한 암호화를 제공합니다.

암호화된 api_keyapi_secret은 모두 Fernetencrypt 메서드를 통해 암호화되며, 복호화는 decrypt 메서드를 통해 수행됩니다.

2. decrypt

복호화는 decrypt_keys 메서드에서 수행됩니다. 저장된 암호화된 데이터를 다시 원본 데이터로 변환합니다.

  1. Salt 복원: 저장된 salt를 복호화하여 원래의 salt 값을 복원합니다.
  2. 키 재생성: 암호화 시 사용된 것과 동일한 saltsome_secret을 결합하여 동일한 대칭 키를 생성합니다.
  3. 데이터 복호화: Fernet 라이브러리를 사용하여 암호화된 api_keyapi_secret을 복호화하여 원본 데이터를 복원합니다.

salt를 secret_manager 와 결합하여 암호화 키를 다루기 때문에 괜찮은 보안이라고 생각합니다.

그러나 시크릿 키의 노출이 발생하면 전체 암호화 시스템이 취약해질 수 있으므로, 시크릿 키를 안전하게 관리하는 것도 중요할 것 같습니다.

3. 관련 링크