Build an Encrypted 파일 공유 App from Scratch
Step-by-step tutorial to build an encrypted 파일 공유 application. Frontend 암호화, 안전한 backend, and deployment walkthrough.
개인정보 보호법(PIPA) 제24조와 개인정보보호위원회(PIPC) 가이드라인은 암호화를 개인정보 보호의 기술적 안전 조치로 명시합니다. 암호화된 파일 공유 앱을 구축한다는 것은 서버가 아닌 브라우저에 암호화를 배치한다는 의미입니다. 최소 스택: Web Crypto API로 AES-256-GCM 암호화하는 Vite 기반 React 프론트엔드, 모든 업로드를 불투명한 블롭으로 취급하는 Fastify 백엔드, S3 호환 스토리지(Cloudflare R2 또는 Backblaze B2), 복호화 키가 # 뒤에 위치해 서버에 도달하지 않는 짧은 공유 URL. 약 400줄의 코드로 작동하는 버전을 구축하고 월 $5 미만으로 호스팅할 수 있습니다.
실제로 중요한 아키텍처 결정
가장 중요한 결정은 암호화 키의 위치입니다. 서버에 한 번이라도 닿으면 종단 간 암호화가 아니라 추가 단계가 있는 서버 측 암호화입니다. 올바른 패턴: 브라우저에서 무작위 256비트 키를 생성하고, 파일을 암호화하고, 암호문을 업로드하고, URL 프래그먼트에 키를 넣습니다(https://yourapp.com/f/abc123#k=base64key). 브라우저는 HTTP 요청에 프래그먼트를 전송하지 않으므로 키는 클라이언트 측에 유지됩니다.
두 번째 결정: 청크 업로드. 100MB 이상의 파일에는 재개 가능한 멀티파트 업로드가 필요합니다. S3의 멀티파트 API는 최소 5MB 청크와 최대 10,000개 파트를 지원해 50GB 한도를 제공합니다.
프론트엔드 암호화 파이프라인
키를 생성하고 AES-256-GCM으로 암호화하여 암호문 블롭과 URL 프래그먼트용 base64 키를 생성하세요:
async function encryptFile(file: File) {
const key = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const plaintext = await file.arrayBuffer();
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv }, key, plaintext
);
const rawKey = await crypto.subtle.exportKey('raw', key);
const blob = new Blob([iv, new Uint8Array(ciphertext)]);
return { blob, keyBase64: toBase64(rawKey) };
}
50MB 이상의 파일에는 4MB 청크를 암호화하고 ReadableStream에 추가하는 스트리밍 버전으로 교체하세요. 모바일 Safari 힙은 단일 ArrayBuffer에서 약 400MB 이상을 처리하면 충돌합니다.
업로드 엔드포인트 설계
백엔드는 유용한 정보를 알아서는 안 됩니다. 암호문을 POST로 받아, 무작위 16자 URL 안전 ID를 생성하고, 만료 시간과 함께 SQLite에 저장하며, 본문을 S3로 직접 스트리밍하세요:
fastify.post('/upload', async (req, reply) => {
const id = nanoid(16);
const key = `blobs/${id}`;
const upload = new Upload({
client: s3,
params: { Bucket: 'hexa-transfers', Key: key, Body: req.raw }
});
await upload.done();
db.prepare('INSERT INTO shares VALUES (?, ?, ?, ?, 0, ?)').run(
id, key, req.headers['content-length'], Date.now() + 7*86400*1000, 10
);
return { id };
});
기본 만료 7일, 최대 다운로드 10회. 제한이 없으면 스토리지가 무제한 증가하므로 공격적인 기본값이 필요합니다.
프래그먼트 키로 공유 링크 생성
업로드가 완료되면 클라이언트 측에서 공유 URL을 구성하세요:
const { id } = await uploadResponse.json();
const shareUrl = `${location.origin}/f/${id}#k=${keyBase64}`;
그 프래그먼트는 절대 사용자의 브라우저를 떠나지 않습니다. 수신자가 링크를 클릭하면 React 앱이 window.location.hash를 읽어 키를 파싱하고, 암호문을 가져와 로컬에서 복호화합니다. 서버가 평문에 접근하는 유일한 방법은 악성 JavaScript를 사용자에게 푸시하는 것이므로, 엄격한 콘텐츠 보안 정책과 서브리소스 해시 고정이 중요합니다.
다운로드 흐름 구현
다운로드 페이지에서 암호문을 스트림으로 가져와 청크 정렬로 복호화하고, StreamSaver.js나 File System Access API로 복호화된 바이트를 RAM이 아닌 디스크로 직접 내보내세요.
배포와 저렴한 운영
프론트엔드: Cloudflare Pages나 Vercel(둘 다 무료 티어). 백엔드: Caddy(자동 TLS 1.3) 뒤에서 Node 22를 실행하는 단일 $5 DigitalOcean 드롭릿. 스토리지: Cloudflare R2(이그레스 무료). WeTransfer, Smash, SwissTransfer는 AWS 이그레스로 막대한 비용을 지불하지만 R2는 경제학을 바꿉니다.
Caddy의 엄격한 헤더 설정: Strict-Transport-Security, Content-Security-Policy: default-src 'self'; script-src 'self', X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer. 이는 리퍼러나 주입된 스크립트를 통한 URL 프래그먼트 누출 벡터를 차단합니다.
기본적으로 개인정보 친화적인 설계
서버가 항상 암호문만 보기 때문에 GDPR 제4조에 해당하는 개인정보가 거의 없습니다. 개인정보 보호법 준수 측면에서도 마찬가지입니다. 그래도 프로세서 관계(R2와 DigitalOcean), 최대 30일 블롭 보존, 공유 링크에 키가 포함되어 있어 신뢰할 수 있는 채널로 전송해야 한다는 명확한 공지를 문서화하세요. IP 신원을 기록하지 않고 시간당 IP당 50개의 업로드로 레이트 리밋을 설정하세요.
HexaTransfer는 본질적으로 이 아키텍처로 구축됩니다—브라우저의 Web Crypto, 불투명한 블롭 스토리지, URL 프래그먼트의 키, 서버에 평문 없음.
https://hexatransfer.com — 무료, 계정 불필요, 최대 10GB.
엔드투엔드 암호화로 대용량 파일을 안전하게 전송
엔드투엔드 암호화로 최대 10GB의 파일을 무료로 전송하세요. 계정이 필요하지 않습니다. 업로드 전에 브라우저에서 파일이 암호화되어 다른 사람은 읽을 수 없습니다.
파일 보내기