클라이언트 측 암호화를 이용한 제로 지식 파일 공유 플랫폼 구축

발행: (2025년 12월 10일 오후 07:56 GMT+9)
7 min read
원문: Dev.to

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);
}
Back to Blog

관련 글

더 보기 »

2025년 핀테크 앱 개발 비용

Fintech는 니치에서 메인스트림으로 이동했습니다. 2025년에는 사용자가 즉시 온보딩, 실시간 결제, 스마트 분석, 그리고 은행 수준 보안을 기본으로 기대합니다....