클라이언트 측 암호화를 이용한 제로 지식 파일 공유 플랫폼 구축
Source: Dev.to
🎯 핵심 문제
전통적인 파일‑공유 서비스는 서버‑사이드 암호화를 사용합니다:
- 파일이 서버에서 암호화됩니다
- 서비스가 암호화 키를 보관합니다
- 파일을 언제든지 복호화할 수 있습니다(스캔, 규정 준수, 법적 요청 등)
이로 인해 근본적인 신뢰 문제가 발생합니다: 회사의 보안, 정책 및 의도에 의존하게 됩니다.
💡 솔루션: 제로‑지식 아키텍처
FileBee는 제로‑지식 접근 방식을 적용한 클라이언트‑사이드 암호화를 구현합니다:
- 업로드 전에 브라우저에서 암호화가 이루어집니다
- 키는 IndexedDB에 로컬로 저장됩니다
- 서버는 복호화할 수 없는 암호화된 블롭만 저장합니다
- 사용자가 키를 삭제하면 파일은 영구적으로 접근 불가능해집니다
- 데이터베이스가 유출되더라도 공격자는 암호화된 데이터만 얻습니다
🏗️ 기술 아키텍처
스택 개요
| 계층 | 기술 |
|---|---|
| 프론트엔드 | Angular 15+ |
| 스토리지 | IndexedDB (키), Cloud Storage (암호화된 파일) |
| 암호화 | Web Crypto API |
| UI 프레임워크 | Angular Material |
핵심 구성 요소
클라이언트 측 암호화 흐름
// Simplified encryption flow
async function encryptFile(file, encryptionKey) {
const fileBuffer = await file.arrayBuffer();
const iv = crypto.getRandomValues(new Uint8Array(12)); // GCM IV
const encryptedData = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv
},
encryptionKey,
fileBuffer
);
// Prepend IV to encrypted data for decryption later
return concatenate(iv, new Uint8Array(encryptedData));
}
키 생성 및 저장
// Generate encryption key for user
async function generateEncryptionKey() {
return await crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256
},
true, // extractable
['encrypt', 'decrypt']
);
}
// Store in IndexedDB (never sent to server)
async function storeKeyLocally(keyId, key) {
const exportedKey = await crypto.subtle.exportKey('jwk', key);
await db.keys.put({
id: keyId,
key: exportedKey,
createdAt: Date.now()
});
}
업로드 흐름 아키텍처
사용자가 파일을 선택합니다
↓
암호화 키 생성/조회 (IndexedDB)
↓
브라우저에서 파일 암호화 (Web Crypto API)
↓
암호화된 블롭을 서버에 업로드
↓
서버가 짧은 URL을 생성
↓
URL 및 QR 코드를 사용자에게 반환
↓
사용자가 URL과 암호화 키를 별도로 공유
다운로드 흐름 아키텍처
사용자가 URL을 엽니다
↓
암호화 키 입력 요청
↓
서버에서 암호화된 블롭 가져오기
↓
제공된 키로 브라우저에서 복호화
↓
복호화된 파일 다운로드 트리거
🔐 구현된 보안 기능
AES‑256‑GCM 암호화
const algorithm = {
name: 'AES-GCM',
length: 256
};
- 강력한 256비트 암호화
- 내장 인증(변조 방지)
- 파일당 고유 IV 사용
로컬 키 관리
// IndexedDB schema for key storage
interface EncryptionKey {
id: string; // Unique key identifier
key: JsonWebKey; // Exported key data
createdAt: number; // Timestamp
files: string[]; // Associated file IDs
}
키는 브라우저를 떠나지 않습니다.
자동 삭제
// Pseudo‑code for cleanup job
async function cleanupExpiredFiles() {
const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
const expiredFiles = await db.files
.where('uploadedAt')
.below(sevenDaysAgo)
.toArray();
for (const file of expiredFiles) {
await storage.deleteFile(file.id);
await db.files.delete(file.id);
}
}
// 매시간 실행
setInterval(cleanupExpiredFiles, 60 * 60 * 1000);
키 삭제 = 데이터 파괴
async function deleteEncryptionKey(keyId) {
// Get all files associated with this key
const key = await db.keys.get(keyId);
// Delete files from server
for (const fileId of key.files) {
await api.deleteFile(fileId);
}
// Delete key from IndexedDB
await db.keys.delete(keyId);
// Files are now permanently inaccessible
}
🎨 사용자 경험 과제
과제 1: 키 관리
문제: 사용자는 무작위 암호화 키(k3j2h4kj234h23k4j23h4k23j4h)를 저장해야 합니다.
해결책:
- 업로드 후 키를 눈에 띄게 표시
- 클립보드 복사 버튼 제공
- 모바일 공유를 위한 QR 코드 생성
- 키 삭제 전 사용자에게 경고
@Component({
selector: 'app-encryption-key-display',
template: `
<mat-card>
<mat-icon>vpn_key</mat-icon>
<pre>{{ encryptionKey }}</pre>
<button mat-icon-button (click)="copyKey()">
<mat-icon>content_copy</mat-icon>
</button>
<mat-icon color="warn">warning</mat-icon>
<p>Save this key! Without it, files cannot be decrypted.</p>
</mat-card>
`
})
과제 2: 대용량 파일 처리
문제: 브라우저에서 50 MB 파일을 암호화하면 UI가 멈출 수 있습니다.
해결책: 암호화를 위해 Web Workers를 사용합니다.
// encryption.worker.ts
self.addEventListener('message', async (e) => {
const { file, key } = e.data;
const encrypted = await encryptFile(file, key);
self.postMessage({ encrypted, progress: 100 });
});
// Main thread
const worker = new Worker('./encryption.worker.ts');
worker.postMessage({ file, key });
worker.addEventListener('message', (e) => {
uploadEncryptedFile(e.data.encrypted);
});
과제 3: 폴더 업로드
문제: 각 파일을 암호화하면서 디렉터리 구조를 유지해야 합니다.
해결책: 메타데이터를 별도로 저장합니다.
interface FolderMetadata {
files: Array<any>;
commonUrl?: string; // Optional common URL for all files
}
// Encrypt and upload folder
async function uploadFolder(folder: File[]): Promise<any> {
const metadata: FolderMetadata = { files: [] };
for (const file of folder) {
const encrypted = await encryptFile(file, key);
const { id } = await uploadToServer(encrypted);
metadata.files.push({
path: file.webkitRelativePath || file.name,
encryptedId: id,
size: file.size,
mimeType: file.type
});
}
return metadata;
}
📊 스토리지 제한 및 최적화
- 파일당 50 MB
- 사용자당 총 250 MB 스토리지
- 7일 자동 삭제
스토리지 할당량 확인
async function checkStorageQuota(user) {
const userFiles = await db.files
.where('userId')
.equals(user)
.toArray();
const totalSize = userFiles.reduce((sum, f) => sum + f.size, 0);
const MAX_STORAGE = 250 * 1024 * 1024; // 250 MB
return totalSize MAX_FILE_SIZE) {
throw new Error(`File exceeds 50 MB limit: ${file.name}`);
}
return true;
}
🎯 QR 코드 생성
모바일 공유를 쉽게 할 수 있도록 QR 코드를 자동 생성합니다:
import * as QRCode from 'qrcode';
// Example: generate a data URL
async function generateQrCode(url: string): Promise<string> {
return await QRCode.toDataURL(url);
}