Alert

์ด ๊ธ€์€ Claude Code์˜ ๋„์›€์„ ๋ฐ›์•„ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค

TL;DR

  • cryptography๋Š” Python ๋Œ€ํ‘œ ์•”ํ˜ธํ™” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
  • Fernet์€ ๋Œ€์นญ ํ‚ค ๊ธฐ๋ฐ˜์˜ ๊ฐ„ํŽธํ•œ ์•”ํ˜ธํ™” ๋„๊ตฌ
  • ํ‚ค ์ƒ์„ฑ โ†’ ์•”ํ˜ธํ™” โ†’ ๋ณตํ˜ธํ™” 3๋‹จ๊ณ„๋กœ ๋™์ž‘
  • ํ‚ค ๊ด€๋ฆฌ(ํ™˜๊ฒฝ๋ณ€์ˆ˜, ํŒŒ์ผ ๋ถ„๋ฆฌ)๊ฐ€ ๋ณด์•ˆ์˜ ํ•ต์‹ฌ
  • ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ๋‚˜ ์„ธ๋ฐ€ํ•œ ์ œ์–ด๊ฐ€ ํ•„์š”ํ•˜๋ฉด AES-GCM ๋“ฑ ์ €์ˆ˜์ค€ API ์‚ฌ์šฉ

1. cryptography ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ž€

Python์—์„œ ์•”ํ˜ธํ™”๋ฅผ ๋‹ค๋ฃฐ ๋•Œ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‹ค. ํ•ด์‹ฑ, ๋Œ€์นญ/๋น„๋Œ€์นญ ์•”ํ˜ธํ™”, ์ธ์ฆ์„œ ์ฒ˜๋ฆฌ ๋“ฑ ์•”ํ˜ธํ™” ๊ด€๋ จ ๊ธฐ๋Šฅ์„ ํญ๋„“๊ฒŒ ์ œ๊ณตํ•œ๋‹ค. ์›น ํ†ต์‹ ์—์„œ ์ด๋Ÿฐ ์•”ํ˜ธํ™”๊ฐ€ ์–ด๋–ป๊ฒŒ ์“ฐ์ด๋Š”์ง€๋Š” TLS ์ฐธ๊ณ .

pip install cryptography
 
# uv ์‚ฌ์šฉ ์‹œ
uv add cryptography

cryptography๋Š” ํฌ๊ฒŒ ๋‘ ๊ฐ€์ง€ ๋ ˆ์ด์–ด๋กœ ๋‚˜๋‰œ๋‹ค.

  • High-level (Fernet ๋“ฑ) : ๋ณต์žกํ•œ ์„ค์ • ์—†์ด ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” API
  • Low-level (hazmat) : AES, RSA ๋“ฑ ์•”ํ˜ธํ™” ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์ง์ ‘ ๋‹ค๋ฃจ๋Š” API. ์ด๋ฆ„ ๊ทธ๋Œ€๋กœ ์œ„ํ—˜ํ•  ์ˆ˜ ์žˆ์–ด์„œ ์•”ํ˜ธํ™”์— ๋Œ€ํ•œ ์ดํ•ด๊ฐ€ ํ•„์š”ํ•˜๋‹ค

์ด ๊ธ€์—์„œ๋Š” High-level API์ธ Fernet์„ ๋‹ค๋ฃฌ๋‹ค.


2. ๋Œ€์นญ ํ‚ค ์•”ํ˜ธํ™”๋ž€

๊ธฐ์กด ๋…ธํŠธ ์ฐธ๊ณ 

๋Œ€์นญ ํ‚ค์™€ ๊ณต๊ฐœ ํ‚ค ์•”ํ˜ธํ™”์˜ ๊ฐœ๋…์€ 17. ์•ˆ์ „์„ฑ์„ ์œ„ํ•œ ๊ธฐ์ˆ  ์—์„œ๋„ ๋‹ค๋ฃจ๊ณ  ์žˆ๋‹ค

์•”ํ˜ธํ™”์™€ ๋ณตํ˜ธํ™”์— ๊ฐ™์€ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.

๋น„์œ ๋กœ ์ดํ•ดํ•˜๊ธฐ

์ž๋ฌผ์‡ ๊ฐ€ ๋‹ฌ๋ฆฐ ๊ธˆ๊ณ ๋ฅผ ๋– ์˜ฌ๋ ค ๋ณด์ž.

  1. A๊ฐ€ ๊ธˆ๊ณ ์— ๋น„๋ฐ€ ๋ฌธ์„œ๋ฅผ ๋„ฃ๊ณ  ์—ด์‡ ๋กœ ์ž ๊ทผ๋‹ค
  2. ์ž ๊ธด ๊ธˆ๊ณ ๋ฅผ B์—๊ฒŒ ๋ณด๋‚ธ๋‹ค
  3. 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 ๋“ฑ์„ ์ง์ ‘ ๋‹ค๋ค„์•ผ ํ•œ๋‹ค.