- Published on
Fernet와 AWS secret manager를 활용한 암호화 및 복호화 방법
진행한 프로젝트에서 서드파티API(키와 시크릿)같은 민감한 정보를 유저별로 저장해야 했습니다.
AWS에서 동작하는 PrivateDB 내에 저장하여 사용한다고 하지만, 만에 하나 DB가 노출될 경우 심각한 문제를 초래할 수 있다고 생각했습니다.
특히 해당 부분이 (간접?)금융 관련 데이터&서비스에 접근하는 부분이었기 때문에 API의 경우 더욱 신중하게 다뤄야 한다고 생각했습니다.
이 게시글에서는 Django 애플리케이션에서 API 키와 시크릿을 안전하게 저장하는 방법을 기록합니다.
Django Model
참고사항 with Code
salt
: 암호화 및 복호화 시 사용되는 임의의 데이터를 저장합니다.exchange
와is_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
- Salt 생성:
os.urandom(16)
을 사용하여 16바이트 길이의 랜덤 데이터를 생성하고, 이를salt
로 사용합니다. - Some Secret과 결합:
combine_secret_and_salt
메서드를 사용하여 생성된 salt 와 Secret Manager에서 가져온some_secret
을 결합합니다. - 키 생성: 결합된
salt_with_some_secret
값을PBKDF2HMAC
함수에 전달하여 대칭 키를 생성합니다. 이 대칭 키는 데이터를 암호화하고 복호화하는 데 사용됩니다. - 데이터 암호화:
Fernet
라이브러리를 사용하여api_key
와api_secret
을 암호화합니다. 암호화된 데이터는 안전하게 저장할 수 있습니다. - Salt 저장: 복호화 시 동일한 키를 생성하기 위해 사용된
salt
를 함께 저장합니다.
1.1 Fernet
이 코드에서 사용된 Fernet
은 Python의 cryptography
라이브러리에서 제공되는 대칭 키 암호화 도구입니다.
Fernet
은 데이터를 암호화하고 복호화하는데 동일한 비밀 키를 사용하는 방식으로, 안전하고 간편한 암호화를 제공합니다.
암호화된 api_key
와 api_secret
은 모두 Fernet
의 encrypt
메서드를 통해 암호화되며, 복호화는 decrypt
메서드를 통해 수행됩니다.
2. decrypt
복호화는 decrypt_keys
메서드에서 수행됩니다. 저장된 암호화된 데이터를 다시 원본 데이터로 변환합니다.
- Salt 복원: 저장된
salt
를 복호화하여 원래의salt
값을 복원합니다. - 키 재생성: 암호화 시 사용된 것과 동일한
salt
와some_secret
을 결합하여 동일한 대칭 키를 생성합니다. - 데이터 복호화:
Fernet
라이브러리를 사용하여 암호화된api_key
와api_secret
을 복호화하여 원본 데이터를 복원합니다.
salt
를 secret_manager 와 결합하여 암호화 키를 다루기 때문에 괜찮은 보안이라고 생각합니다.
그러나 시크릿 키의 노출이 발생하면 전체 암호화 시스템이 취약해질 수 있으므로, 시크릿 키를 안전하게 관리하는 것도 중요할 것 같습니다.