Przejdź do treści
HexaTransfer
Wróć do bloga
Zagadnienia techniczne

Zbuduj an Zaszyfrowany Udostępnianie plików App from Scratch

Step-by-step tutorial to build an encrypted plik sharing application. Frontend encryption, secure backend, i deployment walkthrough.

RODO wymaga stosowania "odpowiednich środków technicznych" chroniących dane osobowe — a szyfrowanie end-to-end po stronie klienta jest jednym z najskuteczniejszych. Budowanie zaszyfrowanej aplikacji do udostępniania plików oznacza umieszczenie kryptografii w przeglądarce, nie na serwerze. Minimalny stack: frontend oparty na Vite i React szyfrujący przez AES-256-GCM za pomocą Web Crypto API, backend Fastify traktujący każdy upload jako nieprzejrzysty blob, storage kompatybilny z S3 (Cloudflare R2 lub Backblaze B2) i krótki adres URL udostępniania, gdzie klucz deszyfrowania żyje za #, żeby nigdy nie docierał do serwera. Działającą wersję można wypuścić w około 400 liniach kodu i hostować za poniżej 5 dolarów miesięcznie.

Decyzje architektoniczne, które naprawdę mają znaczenie

Najważniejsza decyzja to, gdzie żyje klucz szyfrowania. Jeśli kiedykolwiek dotknie twojego serwera, nie masz szyfrowania end-to-end — masz szyfrowanie po stronie serwera z dodatkowymi krokami. Właściwy wzorzec: generuj losowy 256-bitowy klucz w przeglądarce, zaszyfruj plik nim, prześlij szyfrogram i umieść klucz we fragmencie URL (https://yourapp.com/f/abc123#k=base64key). Przeglądarki nigdy nie wysyłają fragmentów w żądaniach HTTP, więc klucz pozostaje po stronie klienta.

Drugi ważny wybór: fragmentowane uploady. Dla plików powyżej 100 MB potrzebujesz wznawianych uploadów wieloczęściowych, inaczej każde niestabilne Wi-Fi niszczy transfer. Multipart API S3 obsługuje minimalne fragmenty 5 MB i do 10 000 części, co daje pułap 50 GB. Planuj to od pierwszego dnia.

Konfiguracja szkieletu projektu

Zacznij od dwóch pakietów: frontend Vite + React i backend Fastify. Frontend obsługuje całą kryptografię, backend obsługuje storage i metadane. Używaj TypeScript dla bezpieczeństwa typów na ArrayBuffer i CryptoKey.

pnpm create vite@latest frontend -- --template react-ts
pnpm create fastify backend

Dodaj do backendu: @fastify/multipart, @aws-sdk/client-s3, @aws-sdk/s3-request-presigner i better-sqlite3 dla metadanych udostępniania. Utrzymuj schemat SQLite minimalny: shares(id, object_key, size_bytes, expires_at, download_count, max_downloads). Bez nazw plików, bez danych użytkownika, bez adresów IP.

Pisanie pipeline szyfrowania frontendu

Generuj klucz, szyfruj przez AES-256-GCM i produkuj blob szyfrogramu plus klucz base64 dla fragmentu URL:

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) };
}

Dla plików powyżej 50 MB zastąp to wersją strumieniującą szyfrującą fragmenty po 4 MB i dołączającą je do ReadableStream. Stos pamięci mobilnego Safari zakrztusza się na czymkolwiek większym niż około 400 MB w jednym ArrayBuffer.

Projektowanie endpointu upload

Backend powinien nic nie wiedzieć. Przyjmij POST z szyfrogramem, wygeneruj losowy 16-znakowy ID bezpieczny dla URL, przechowaj go w SQLite z terminem ważności i przesyłaj ciało strumieniowo prosto do 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 };
});

Domyślny termin ważności 7 dni, maks. 10 pobrań. Agresywne domyślne wartości są ważne, bo alternatywą jest nieograniczony wzrost storage. Używaj crona do usuwania wygasłych blobów co noc.

Generowanie linków udostępniania z kluczami we fragmencie

Po zakończeniu uploadu zbuduj URL udostępniania po stronie klienta:

const { id } = await uploadResponse.json();
const shareUrl = `${location.origin}/f/${id}#k=${keyBase64}`;

Ten fragment nigdy nie opuszcza przeglądarki użytkownika. Gdy odbiorca kliknie link, aplikacja React odczytuje window.location.hash, parsuje klucz, pobiera szyfrogram i deszyfruje lokalnie. Serwer nie ma drogi do tekstu jawnego poza wstrzyknięciem złośliwego JavaScript — dokładnie dlatego powinieneś dostarczyć ścisłą Content Security Policy.

Implementacja przepływu pobierania

Na stronie pobierania pobierz szyfrogram jako strumień i deszyfruj wyrównując porcje:

const keyRaw = base64ToBytes(location.hash.slice(3));
const key = await crypto.subtle.importKey(
  'raw', keyRaw, { name: 'AES-GCM' }, false, ['decrypt']
);
const resp = await fetch(`/blob/${id}`);
const iv = new Uint8Array(await resp.body.getReader().read().then(r => r.value.slice(0, 12)));
const ct = await resp.arrayBuffer();
const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct.slice(12));
const url = URL.createObjectURL(new Blob([pt]));

Dla dużych plików użyj StreamSaver.js lub File System Access API, żeby odszyfrowane bajty trafiały bezpośrednio na dysk zamiast do RAM.

Wdrożenie całości

Frontend: wypchnij na Cloudflare Pages lub Vercel — oba bezpłatne plany to obsługują. Backend: jeden droplet DigitalOcean za 5 dolarów z Node 22 za Caddy (automatyczny TLS 1.3). Storage: Cloudflare R2 daje zerowe opłaty za wychodzący ruch sieciowy, co jest kluczową cechą przy udostępnianiu plików na dużą skalę. WeTransfer, Smash i SwissTransfer płacą fortuny za egress przez AWS — R2 całkowicie zmienia ekonomikę.

Ustaw ścisłe nagłówki przez Caddy: Strict-Transport-Security, Content-Security-Policy: default-src 'self'; script-src 'self', X-Content-Type-Options: nosniff i Referrer-Policy: no-referrer.

Domyślna zgodność z RODO i UODO

Ponieważ serwer widzi tylko szyfrogram, masz prawie nic, co kwalifikuje się jako dane osobowe zgodnie z art. 4 RODO. Mimo to udokumentuj relacje z podmiotami przetwarzającymi (R2 i DigitalOcean), ustaw maksymalny okres przechowywania blobów na 30 dni i opublikuj jasne powiadomienie, że linki udostępniania zawierają klucze i powinny być przesyłane kanałami, którym nadawca ufa. Dodaj ograniczenie liczby żądań — 50 uploadów na IP na godzinę — żeby zniechęcić do nadużyć bez logowania tożsamości użytkowników.

HexaTransfer jest zbudowany zasadniczo na tej architekturze — Web Crypto w przeglądarce, przechowywanie nieprzejrzystych blobów, klucze we fragmentach URL, zero tekstu jawnego na serwerach. Wypróbuj na https://hexatransfer.com — za darmo, bez konta, maks. 10 GB.

Co dodać po MVP

Gdy podstawowy przepływ działa, najbardziej wartościowe dodatki to: ochrona hasłem na wierzchu klucza fragmentu (PBKDF2 z 600 000 iteracji), powiadomienia email per pobranie przez burner inbox i raportowanie nadużyć pozwalające na flagowanie hasha szyfrogramu bez ujawniania zawartości. Unikaj dodawania kont bez konkretnego powodu — zamieniają twoją aplikację z narzędzia prywatności w zobowiązanie dotyczące danych z dnia na dzień.

Wysyłaj duże pliki bezpiecznie z szyfrowaniem end-to-end

Przesyłaj pliki do 10 GB za darmo z szyfrowaniem end-to-end. Bez rejestracji. Twoje pliki są szyfrowane w przeglądarce przed przesłaniem — nikt inny nie może ich odczytać.

Wyślij plik