Alert
์ด ๊ธ์ Claude Code์ ๋์์ ๋ฐ์ ์์ฑ๋์์ต๋๋ค
TL;DR
cryptography๋ Python ๋ํ ์ํธํ ๋ผ์ด๋ธ๋ฌ๋ฆฌFernet์ ๋์นญ ํค ๊ธฐ๋ฐ์ ๊ฐํธํ ์ํธํ ๋๊ตฌ- ํค ์์ฑ โ ์ํธํ โ ๋ณตํธํ 3๋จ๊ณ๋ก ๋์
- ํค ๊ด๋ฆฌ(ํ๊ฒฝ๋ณ์, ํ์ผ ๋ถ๋ฆฌ)๊ฐ ๋ณด์์ ํต์ฌ
- ๋์ฉ๋ ๋ฐ์ดํฐ๋ ์ธ๋ฐํ ์ ์ด๊ฐ ํ์ํ๋ฉด AES-GCM ๋ฑ ์ ์์ค API ์ฌ์ฉ
1. cryptography ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋
Python์์ ์ํธํ๋ฅผ ๋ค๋ฃฐ ๋ ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉ๋๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค. ํด์ฑ, ๋์นญ/๋น๋์นญ ์ํธํ, ์ธ์ฆ์ ์ฒ๋ฆฌ ๋ฑ ์ํธํ ๊ด๋ จ ๊ธฐ๋ฅ์ ํญ๋๊ฒ ์ ๊ณตํ๋ค. ์น ํต์ ์์ ์ด๋ฐ ์ํธํ๊ฐ ์ด๋ป๊ฒ ์ฐ์ด๋์ง๋ TLS ์ฐธ๊ณ .
pip install cryptography
# uv ์ฌ์ฉ ์
uv add cryptographycryptography๋ ํฌ๊ฒ ๋ ๊ฐ์ง ๋ ์ด์ด๋ก ๋๋๋ค.
- High-level (Fernet ๋ฑ) : ๋ณต์กํ ์ค์ ์์ด ๋ฐ๋ก ์ฌ์ฉํ ์ ์๋ API
- Low-level (hazmat) : AES, RSA ๋ฑ ์ํธํ ์๊ณ ๋ฆฌ์ฆ์ ์ง์ ๋ค๋ฃจ๋ API. ์ด๋ฆ ๊ทธ๋๋ก ์ํํ ์ ์์ด์ ์ํธํ์ ๋ํ ์ดํด๊ฐ ํ์ํ๋ค
์ด ๊ธ์์๋ High-level API์ธ Fernet์ ๋ค๋ฃฌ๋ค.
2. ๋์นญ ํค ์ํธํ๋
๊ธฐ์กด ๋ ธํธ ์ฐธ๊ณ
๋์นญ ํค์ ๊ณต๊ฐ ํค ์ํธํ์ ๊ฐ๋ ์ 17. ์์ ์ฑ์ ์ํ ๊ธฐ์ ์์๋ ๋ค๋ฃจ๊ณ ์๋ค
์ํธํ์ ๋ณตํธํ์ ๊ฐ์ ํค๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ์์ด๋ค.
๋น์ ๋ก ์ดํดํ๊ธฐ
์๋ฌผ์ ๊ฐ ๋ฌ๋ฆฐ ๊ธ๊ณ ๋ฅผ ๋ ์ฌ๋ ค ๋ณด์.
- A๊ฐ ๊ธ๊ณ ์ ๋น๋ฐ ๋ฌธ์๋ฅผ ๋ฃ๊ณ ์ด์ ๋ก ์ ๊ทผ๋ค
- ์ ๊ธด ๊ธ๊ณ ๋ฅผ B์๊ฒ ๋ณด๋ธ๋ค
- B๋ ๊ฐ์ ์ด์ ๋ก ๊ธ๊ณ ๋ฅผ ์ด์ด ๋ฌธ์๋ฅผ ๊บผ๋ธ๋ค
์ฌ๊ธฐ์ ํต์ฌ์ A์ B๊ฐ ๋๊ฐ์ ์ด์ ๋ฅผ ๊ฐ์ง๊ณ ์์ด์ผ ํ๋ค๋ ๊ฒ์ด๋ค. ์ด์ ๊ฐ ํ๋๋ฟ์ด๊ณ , ์ด ์ด์ ๊ฐ ์ ๊ทธ๋ ๊ฒ๊ณผ ์ฌ๋ ๊ฒ ๋ชจ๋์ ์ฌ์ฉ๋๋ค. ์ด๊ฒ์ด โ๋์นญโ์ด๋ผ๋ ์ด๋ฆ์ ์ด์ ๋ค.
๋ฐ๋ฉด ๋น๋์นญ ํค ์ํธํ๋ ์ฐ์ฒดํต๊ณผ ๋น์ทํ๋ค. ๋๊ตฌ๋ ์ฐํธ๋ฌผ์ ๋ฃ์ ์ ์์ง๋ง(๊ณต๊ฐ ํค๋ก ์ํธํ), ์ฐ์ฒดํต์ ์ด์ด์ ๊บผ๋ด๋ ๊ฒ์ ์ด์ ๋ฅผ ๊ฐ์ง ์ฃผ์ธ๋ง ๊ฐ๋ฅํ๋ค(๊ฐ์ธ ํค๋ก ๋ณตํธํ).
๋์นญ ํค์ ํน์ง
| ์ฅ์ | ๋จ์ |
|---|---|
| ์๋๊ฐ ๋น ๋ฅด๋ค | ํค๋ฅผ ์์ ํ๊ฒ ์ ๋ฌํ๊ธฐ ์ด๋ ต๋ค |
| ๊ตฌํ์ด ๋จ์ํ๋ค | ํค๊ฐ ์ ์ถ๋๋ฉด ๋ชจ๋ ๋ฐ์ดํฐ๊ฐ ๋ ธ์ถ๋๋ค |
| ๋์ฉ๋ ๋ฐ์ดํฐ์ ์ ํฉํ๋ค | ํต์ ์๋๋ง๋ค ๋ณ๋์ ํค๊ฐ ํ์ํ๋ค |
์ค๋ฌด์์๋
๋์นญ ํค ์ํธํ๋ ์ฃผ๋ก ๋ฐ์ดํฐ ์์ฒด๋ฅผ ์ํธํํ ๋ ์ฌ์ฉํ๋ค. ์๋๋ฐฉ์๊ฒ ํค๋ฅผ ์ ๋ฌํด์ผ ํ๋ ์ํฉ์ด๋ผ๋ฉด ๋น๋์นญ ํค๋ก ๋์นญ ํค๋ฅผ ์ํธํํด์ ๋ณด๋ด๋ ํ์ด๋ธ๋ฆฌ๋ ๋ฐฉ์์ ์ด๋ค.
3. Fernet ์ฌ์ฉ๋ฒ
Fernet์ cryptography๊ฐ ์ ๊ณตํ๋ ๋์นญ ํค ์ํธํ ๋๊ตฌ๋ค. ๋ด๋ถ์ ์ผ๋ก AES-128-CBC + HMAC-SHA256์ ์ฌ์ฉํด์ ์ํธํ์ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ์ ๋์์ ์ฒ๋ฆฌํ๋ค.
๊ธฐ๋ณธ ํ๋ฆ
from cryptography.fernet import Fernet
# 1. ํค ์์ฑ
key = Fernet.generate_key()
print(key)
# b'ZmDfcTF7_60GrrY4vBGJSVgmYR0yGH8rrOamiLkI6mA='
# URL-safe base64๋ก ์ธ์ฝ๋ฉ๋ 32๋ฐ์ดํธ ํค
# 2. Fernet ๊ฐ์ฒด ์์ฑ
f = Fernet(key)
# 3. ์ํธํ
token = f.encrypt(b"hello world")
print(token)
# b'gAAAAABm...' ์ํธํ๋ ํ ํฐ
# 4. ๋ณตํธํ
plain = f.decrypt(token)
print(plain)
# b'hello world'encrypt/decrypt๋ bytes๋ง ๋ฐ๋๋ค
๋ฌธ์์ด์ ์ํธํํ๋ ค๋ฉด
.encode()๋ก bytes ๋ณํ์ด ํ์ํ๋คmessage = "๋น๋ฐ ๋ฉ์์ง" token = f.encrypt(message.encode("utf-8")) plain = f.decrypt(token).decode("utf-8")
์๋ชป๋ ํค๋ก ๋ณตํธํํ๋ฉด?
wrong_key = Fernet.generate_key()
wrong_f = Fernet(wrong_key)
wrong_f.decrypt(token)
# cryptography.fernet.InvalidToken ์์ธ ๋ฐ์ํค๊ฐ ๋ค๋ฅด๊ฑฐ๋ ํ ํฐ์ด ๋ณ์กฐ๋์์ผ๋ฉด InvalidToken ์์ธ๊ฐ ๋ฐ์ํ๋ค. ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ๊น์ง ๊ฒ์ฆํ๊ธฐ ๋๋ฌธ์ ํ ํฐ์ ์ผ๋ถ๋ง ๋ฐ๊ฟ๋ ๋ณตํธํ์ ์คํจํ๋ค.
ํ ํฐ ๋ง๋ฃ ์๊ฐ ์ค์
Fernet ํ ํฐ์๋ ์์ฑ ์๊ฐ์ด ํฌํจ๋์ด ์์ด์, ๋ณตํธํ ์ TTL(Time To Live)์ ์ง์ ํ ์ ์๋ค.
import time
token = f.encrypt(b"temporary data")
time.sleep(5)
# 3์ด TTL ์ค์ โ ์ด๋ฏธ 5์ด๊ฐ ์ง๋ฌ์ผ๋ฏ๋ก ์คํจ
f.decrypt(token, ttl=3)
# cryptography.fernet.InvalidToken์์ ํ ํฐ์ด๋ 1ํ์ฑ ์ธ์ฆ ์ฝ๋์ ์ ์ฉํ๋ค.
4. ํค ๊ด๋ฆฌ ์ค๋ฌด ํจํด
Fernet ์ํธํ์์ ๊ฐ์ฅ ์ค์ํ ๊ฒ์ ์๊ณ ๋ฆฌ์ฆ์ด ์๋๋ผ ํค ๊ด๋ฆฌ๋ค. ํค๋ฅผ ์ฝ๋์ ํ๋์ฝ๋ฉํ๋ ์๊ฐ ์ํธํ์ ์๋ฏธ๊ฐ ์ฌ๋ผ์ง๋ค.
ํ๊ฒฝ๋ณ์๋ก ๊ด๋ฆฌ
import os
from cryptography.fernet import Fernet
key = os.environ.get("FERNET_KEY")
if key is None:
raise RuntimeError("FERNET_KEY ํ๊ฒฝ๋ณ์๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค")
f = Fernet(key.encode())# ํค ์์ฑ ํ ํ๊ฒฝ๋ณ์์ ๋ฑ๋ก
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
export FERNET_KEY="์์ฑ๋_ํค_๊ฐ"ํค ํ์ผ ๋ถ๋ฆฌ
from pathlib import Path
from cryptography.fernet import Fernet
KEY_PATH = Path("secret.key")
def load_or_create_key() -> bytes:
if KEY_PATH.exists():
return KEY_PATH.read_bytes()
key = Fernet.generate_key()
KEY_PATH.write_bytes(key)
return key
key = load_or_create_key()
f = Fernet(key)ํค ํ์ผ์ ๋ฐ๋์ .gitignore์ ์ถ๊ฐ
*.key secret.key
ํค ๋กํ ์ด์
ํค๋ฅผ ์ฃผ๊ธฐ์ ์ผ๋ก ๊ต์ฒดํด์ผ ํ ๋๋ MultiFernet์ ์ฌ์ฉํ๋ค. ์ ํค๋ก ์ํธํํ๋ ์ด์ ํค๋ก ์ํธํ๋ ๋ฐ์ดํฐ๋ ๋ณตํธํํ ์ ์๋ค.
from cryptography.fernet import Fernet, MultiFernet
old_key = Fernet(old_key_bytes)
new_key = Fernet(new_key_bytes)
# ์ฒซ ๋ฒ์งธ ํค๊ฐ ์ํธํ์ ์ฌ์ฉ๋๊ณ , ๋ณตํธํ๋ ์์๋๋ก ์๋
multi = MultiFernet([new_key, old_key])
# ์ ํค๋ก ์ํธํ
token = multi.encrypt(b"data")
# ๊ธฐ์กด ํ ํฐ๋ ๋ณตํธํ ๊ฐ๋ฅ
plain = multi.decrypt(old_token)
# ๊ธฐ์กด ํ ํฐ์ ์ ํค๋ก ์ฌ์ํธํ
new_token = multi.rotate(old_token)5. ์ค์ ์์
API ํค ์ํธํ ์ ์ฅ
์ค์ ํ์ผ์ด๋ DB์ API ํค๋ฅผ ์ ์ฅํ ๋ ํ๋ฌธ ๋์ ์ํธํํด์ ์ ์ฅํ๋ ํจํด์ด๋ค.
import json
from pathlib import Path
from cryptography.fernet import Fernet
def save_api_key(api_key: str, fernet: Fernet, path: str = "config.enc.json"):
encrypted = fernet.encrypt(api_key.encode()).decode()
Path(path).write_text(json.dumps({"api_key": encrypted}))
def load_api_key(fernet: Fernet, path: str = "config.enc.json") -> str:
data = json.loads(Path(path).read_text())
return fernet.decrypt(data["api_key"].encode()).decode()
# ์ฌ์ฉ
key = Fernet.generate_key()
f = Fernet(key)
save_api_key("sk-abc123...", f)
loaded = load_api_key(f)
print(loaded) # sk-abc123...ํ์ผ ์ํธํ/๋ณตํธํ
from pathlib import Path
from cryptography.fernet import Fernet
def encrypt_file(src: str, dst: str, fernet: Fernet):
data = Path(src).read_bytes()
Path(dst).write_bytes(fernet.encrypt(data))
def decrypt_file(src: str, dst: str, fernet: Fernet):
data = Path(src).read_bytes()
Path(dst).write_bytes(fernet.decrypt(data))
# ์ฌ์ฉ
key = Fernet.generate_key()
f = Fernet(key)
encrypt_file("secret.txt", "secret.txt.enc", f)
decrypt_file("secret.txt.enc", "secret_decrypted.txt", f)๋์ฉ๋ ํ์ผ ์ฃผ์
Fernet์ ๋ฐ์ดํฐ ์ ์ฒด๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ์ฌ๋ ค์ ํ๋ฒ์ ์ํธํํ๋ค. ์๋ฐฑ MB ์ด์์ ํ์ผ์๋ ์ ํฉํ์ง ์๋ค. ์ด๋ฐ ๊ฒฝ์ฐ AES-GCM์ผ๋ก ์ฒญํฌ ๋จ์ ์ํธํ๋ฅผ ์ฌ์ฉํด์ผ ํ๋ค.
6. Fernet์ ํ๊ณ์ ๋์
Fernet์ ๊ฐํธํ์ง๋ง ๋ชจ๋ ์ํฉ์ ์ ํฉํ์ง๋ ์๋ค.
| ํ๊ณ | ์ค๋ช |
|---|---|
| AES-128 ๊ณ ์ | ํค ๊ธธ์ด๋ ์๊ณ ๋ฆฌ์ฆ์ ๋ณ๊ฒฝํ ์ ์๋ค |
| ๋ฉ๋ชจ๋ฆฌ ์ ์ฝ | ์ ์ฒด ๋ฐ์ดํฐ๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ์ฌ๋ฆฌ๋ฏ๋ก ๋์ฉ๋ ํ์ผ์ ๋ถ์ ํฉ |
| ์คํธ๋ฆฌ๋ฐ ๋ถ๊ฐ | ์ฒญํฌ ๋จ์ ์ํธํ๋ฅผ ์ง์ํ์ง ์๋๋ค |
| ์ปค์คํฐ๋ง์ด์ง ๋ถ๊ฐ | IV, ๋ชจ๋, ํจ๋ฉ ๋ฑ์ ์ง์ ์ ์ดํ ์ ์๋ค |
์ด๋ฐ ํ๊ณ๊ฐ ์์ ๋๋ cryptography์ low-level API๋ฅผ ์ฌ์ฉํด์ AES-GCM, RSA ๋ฑ์ ์ง์ ๋ค๋ค์ผ ํ๋ค.