Sandcorp샌드코프 · 웹·AI 제품 개발

새 프로젝트 받는 중 · 갱신 2026.11sandcorp.co.krGitHub

← 팁

인증

해시로만 저장한 접근 코드는 관리자도 다시 볼 수 없다 — 검사용 해시와 보기용 암호문을 따로 둔다

확인은 지금처럼 해시로 하고, 관리자 상세 화면에서 다시 보여 줄 값만 따로 암호화해 둡니다. 키는 저장하지 않고 앱 비밀에서 용도별로 파생합니다.

공유 링크에 붙이는 접근 코드를 비밀번호처럼 해시로만 저장해 두면, 나중에 관리자 화면에서 「이 링크의 코드가 뭐였지?」를 보여 줄 방법이 없습니다. 해시는 일방향이라 되살릴 수 없기 때문입니다. 그렇다고 평문 칸을 하나 더 두면 DB 덤프 하나로 모든 코드가 드러납니다.

해법은 두 칸의 역할을 나누는 것입니다. 검사는 해시 그대로 둡니다. 들어온 코드가 맞는지는 지금처럼 해시와 비교합니다. 그리고 보기용 암호문 칸을 더해, 코드를 발행하거나 다시 발행할 때 해시와 함께 암호문도 씁니다. 관리자 상세 화면만 이 칸을 복호해 보여 줍니다. 목록·API·로그·변경 이력에는 평문도 암호문도 내보내지 않고, 상세 응답에는 Cache-Control: no-store를 붙입니다.

암호화 키는 새로 만들어 저장하지 않고, 이미 있는 앱 비밀(Django라면 SECRET_KEY)에서 용도 라벨을 붙여 HKDF로 파생합니다. 라벨이 다르면 키도 달라서, 나중에 같은 방식으로 다른 비밀(웹훅 주소 등)을 봉인해도 서로 섞이지 않습니다.

import base64
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from django.conf import settings

def _box(label: str) -> Fernet:
    key = HKDF(algorithm=hashes.SHA256(), length=32, salt=None,
               info=label.encode()).derive(settings.SECRET_KEY.encode())
    return Fernet(base64.urlsafe_b64encode(key))

def seal(code: str) -> str:
    return _box("app.access-code").encrypt(code.encode()).decode()

def open_or_none(token: str) -> str | None:
    try:
        return _box("app.access-code").decrypt(token.encode()).decode()
    except InvalidToken:          # 옛 행(암호문 없음)이거나 앱 비밀이 바뀜
        return None

두 가지를 미리 정해 둡니다. 첫째, 이 칸이 생기기 전에 발행한 코드는 되살릴 수 없으니 화면에 「다시 볼 수 없음 — 새 코드를 넣으세요」를 띄우고, 새 코드를 넣을 때 암호문을 채웁니다. 둘째, 앱 비밀을 바꾸면 보기만 안 되고 검사는 계속 됩니다. 검사는 해시로 하기 때문입니다. 이 성질을 운영 문서에 적어 두면 비밀을 교체한 날 놀라지 않습니다.

문의 →