파일 공유 서버가 파일을 읽지 못하도록 만든 방법
Source: Dev.to
대부분의 “암호화된” 파일 공유는 전송 중 및 저장 시 암호화된다는 의미이며, 이는 서버가 키를 보관하고 언제든지 모든 파일을 읽을 수 있다는 정중한 표현이다. 나는 반대의 방식을 원했다: 서버가 내 파일을 저장하지만 실제로는 열 수 없는 방식. 여기 share·me에서 그것이 어떻게 동작하는지, 튜토리얼이 조용히 넘어가는 부분까지 포함한다.
브라우저가 무작위 키를 생성하고, 파일을 클라이언트 측에서 AES‑256‑GCM으로 암호화하며, 키는 여기로 전달된다:
https://share.example/d/AbC123#k=
# 뒤에 오는 모든 내용은 절대 브라우저를 떠나지 않는다. 요청 라인에도, 서버 로그에도, 앞에 두는 프록시의 접근 로그에도 남지 않는다. 링크에 키가 포함되고, 서버는 암호문(파일명 포함)만 본다. 이것이 전부이며, 거의 창피할 정도로 간단하다.
키를 링크에 전혀 넣고 싶지 않다면? 비밀번호 모드에서는 Argon2id(백업으로 PBKDF2)로 키를 파생한다. 잘못된 비밀번호는 서버 측 인증에 실패할 뿐이며, 이 과정은 속도 제한이 걸려 있어 오프라인에서 무차별 대입 공격을 할 수 있는 표면이 없다.
모든 “WebCrypto로 파일 암호화” 튜토리얼은 encrypt(wholeFile)을 호출한다. 5 GB 업로드에 적용하면 탭이 메모리 부족(OOM)으로 죽는다. 실제 파일은 스트리밍해야 한다.
따라서 암호화 패키지는 세분화된 AES‑256‑GCM 스트림 구조를 사용한다: 파일을 청크로 나누고, 각 청크는 HKDF로 파생된 서브키와 순차 논스로 암호화한다. 여기서 내가 겪은 두 가지 중요한 점은 다음과 같다:
- 청크별 논스는 결정적이고 순서가 있어야 한다. 그렇지 않으면 버퍼링하지 않은 스트림을 복호화할 수 없다. 무작위 논스가 아니라 순번을 사용한다.
- AES‑GCM은 키 커밋을 보장하지 않는다. 암호문을 두 개의 서로 다른 키로도 정상 복호화되도록 만들 수 있다. 파일 공유 서비스에서는 이는 큰 위험 요소이므로, 암호문을 정확히 하나의 키에 묶는 키‑커밋 헤더를 추가한다.
브라우저 ↔ API가 직접 스트리밍하고, Rust 서비스는 전체 파일을 메모리에 보관하지 않는다.
브라우저 (클라이언트 측 AES‑256‑GCM)
│
▼
Traefik ──/api/*──► Rust / axum API ──► blobs (disk or S3) + metadata (SQLite/Postgres)
└─────/*──────► Next.js BFF
Traefik을 통해 하나의 오리진만 사용하므로 CORS가 필요 없다. 얇은 Next.js BFF(Server Actions)는 각