· Fundacja Reborn

Udostępnianie notatek i zadań bez kompromisów dla Zero Knowledge

Twórz publiczne linki do migawek (snapshot) swoich notatek lub zadań, zachowując pełne bezpieczeństwo. Wyjaśniamy, jak adres URL przechowuje klucz, dlaczego serwer nie widzi Twoich danych i jak w prosty sposób zabezpieczyć dostęp dodatkowym hasłem.

udostępnianie funkcja zero-knowledge szyfrowanie prywatność
Read in English →

W Reborn Task i Reborn Notes możesz teraz wygenerować publiczny link do pojedynczej notatki lub zadania (tylko do odczytu). Wystarczy, że klikniesz “Udostępnij”. Otrzymujesz URL, który możesz wkleić w czacie czy wysłać mailem. Każdy, kto posiada ten adres, otworzy dokument w przeglądarce bez konieczności zakładania konta.

Najważniejsze jednak jest to, co się nie zmienia: serwer hostujący aplikację nadal nie potrafi przeczytać tego, co udostępniasz. Architektura opiera się na tym samym modelu Zero Knowledge, który chroni Twoje prywatne dane. Został on rozszerzony w taki sposób, aby serwer pozostawał “ślepy”, mimo że informacje są teraz dostępne z publicznego internetu.

Artykuł został podzielony na dwie części. Pierwsza, ogólna, tłumaczy działanie nowej funkcji: jak zabezpieczyć link hasłem, dlaczego technologia pozostaje bezpieczna i co można skonfigurować. Druga część przeznaczona jest dla czytelników zainteresowanych szczegółami technicznymi: jak podróżuje klucz, co dokładnie widzi serwer i jakie warstwy zabezpieczeń wdrożono (tzw. defense-in-depth).

Część 1: Funkcja w praktyce

Co można udostępnić i jak to chronić

Możesz udostępnić pojedynczą notatkę albo zadanie (wraz z podzadaniami). Udostępniona kopia to migawka (snapshot): zamrożony obraz treści z chwili utworzenia linku. Jeśli później edytujesz oryginał, migawka nie ulega zmianie. Jeśli zmienisz zdanie, możesz w każdej chwili unieważnić link.

Podczas tworzenia udostępnienia masz do dyspozycji:

  • Udostępnienie samego tytułu i treści (minimum).
  • Opcjonalnie “nazwę wyświetlaną” - inną etykietę niż tytuł oryginalnego pliku.
  • Opcjonalnie etykietę “udostępnione przez” - krótki tekst identyfikujący nadawcę (np. imię), widoczny na publicznej stronie.

Zabezpieczenie hasłem (rekomendowane) Choć link sam w sobie służy jako kontrola dostępu, najlepszą praktyką jest ustawienie dodatkowego hasła podczas udostępniania. Hasło to jedyny element, który serwer weryfikuje (za pomocą silnego algorytmu Argon2id). Odbiorca będzie musiał je wprowadzić, zanim jego przeglądarka otrzyma zaszyfrowane dane. 10 błędnych prób wpisania hasła skutkuje tymczasową blokadą adresu IP (rate limit), co skutecznie chroni przed atakami typu brute-force, jednocześnie nie zmniejszając Twojego licznika dopuszczalnych otwarć.

Wszystkimi aktywnymi udostępniami zarządzasz w zakładce Ustawienia -> Bezpieczeństwo -> Udostępnienia, a także z poziomu małej ikonki pojawiającej się w nagłówku notatki/zadania.

Dlaczego to nadal Zero Knowledge?

Reborn Apps opiera się na ścisłej zasadzie: serwer nigdy nie widzi Twoich danych w postaci jawnej. Każda notatka, każdy tytuł zadania czy przypisany tag jest szyfrowany na Twoim urządzeniu, zanim je opuści.

Kiedy udostępniasz plik, zasada ta nadal obowiązuje, wprowadzono jednak kluczowy mechanizm. Zamiast korzystać z Twojego prywatnego klucza głównego (który jest unikalny dla konta i nigdy nie opuszcza urządzenia), aplikacja generuje nowy, jednorazowy klucz szyfrujący specjalnie dla tego konkretnego linku. Używa go do zaszyfrowania migawki, wysyła zaszyfrowany plik (blob) na serwer, a sam klucz umieszcza bezpośrednio w adresie URL - w części po znaku # (tzw. fragmencie).

Ten szczegół sprawia, że system działa bezpiecznie. Przeglądarki internetowe z definicji nigdy nie wysyłają na serwer tego, co znajduje się po znaku #. Ta część pozostaje wyłącznie na urządzeniu użytkownika. Zatem gdy odbiorca klika w link:

  1. Jego przeglądarka prosi serwer o zaszyfrowany plik danych (używając części przed #).
  2. Serwer zwraca szyfrogram (dokładnie tak samo, jak w przypadku Twoich prywatnych notatek).
  3. Przeglądarka pobiera klucz z części po #, deszyfruje plik lokalnie i wyświetla treść odbiorcy.

Serwer odpowiada jedynie za przechowywanie i dostarczanie, ale nigdy nie wchodzi w posiadanie klucza. Nawet w przypadku fizycznego skopiowania bazy danych z serwera, osoba atakująca znalazłaby wyłącznie zaszyfrowane bloby bez żadnych kluczy.

”Ale każdy z linkiem może to przeczytać - czy to naprawdę bezpieczne?”

Jeśli utworzysz udostępnienie bez hasła, odpowiedź brzmi: tak, każdy, kto dysponuje pełnym adresem URL (włącznie z częścią po #), odczyta migawkę. To celowe rozwiązanie znane jako wzorzec capability URL (link stanowi poświadczenie dostępu) – stosowane m.in. przez Bitwarden Send czy dawny Firefox Send.

To podejście jest bezpieczne z kilku powodów:

  • Bez części po #, nawet serwer nie potrafi odszyfrować migawki.
  • Logi serwerowe (np. w Nginx) nie zawierają fragmentu po znaku #.
  • Przejście do zewnętrznego linku z udostępnionej strony nie ujawni adresu źródłowego (stosowana jest restrykcyjna polityka Referrer-Policy).
  • Link posiada wystarczającą entropię (96 bitów losowości w ścieżce, 256 bitów losowości w kluczu), co uniemożliwia jego odgadnięcie.

Traktuj link bez hasła jak sam klucz. Jeśli wkleisz go w prywatnym czacie – widzi go tylko odbiorca. Jeśli wrzucisz go na platformę społecznościową – zobaczy go każdy. Dlatego do poufnych danych zawsze zalecamy włączenie wspomnianej wcześniej opcji zabezpieczenia hasłem.

Co jeszcze można skonfigurować

Tworząc udostępnienie, masz do dyspozycji:

UstawienieDziałanie
Hasło (opcjonalne, zalecane)Odbiorca musi je wprowadzić, zanim serwer wyda migawkę. Weryfikowane przez hash Argon2id na serwerze.
Wygaśnięcie (opcjonalne)Link przestaje działać w zadanym czasie (od kilku minut do roku). Po wygaśnięciu zaszyfrowany plik jest trwale usuwany z serwera.
Maksymalna liczba otwarć (opcjonalne)Po N udanych otwarciach link automatycznie ulega unieważnieniu (“przeczytaj i zniszcz”).
Nazwa wyświetlana (opcjonalna)Alternatywny tytuł widoczny dla odbiorcy. Zaszyfrowany, serwer go nie widzi.
Etykieta “udostępnione przez” (opcjonalna)Krótki identyfikator nadawcy. Również przechowywany w formie zaszyfrowanej.

Gdzie faktycznie odbywa się szyfrowanie?

Przepływ danych w skrócie:

  • Na Twoim urządzeniu, podczas tworzenia: generowany jest świeży klucz AES-GCM 256-bit. Migawka jest nim szyfrowana. Klucz ten jest dodatkowo owijany (szyfrowany) Twoim kluczem głównym (abyś mógł podglądać swoje linki na innych urządzeniach). Oba zaszyfrowane pliki trafiają na serwer.
  • Na serwerze: baza przechowuje szyfrogram, zawinięty klucz, hash hasła, datę wygaśnięcia i licznik otwarć. Serwer nie potrafi odszyfrować żadnej z tych wartości.
  • Na urządzeniu odbiorcy (przy otwieraniu): przeglądarka pobiera szyfrogram, wydobywa klucz z fragmentu URL i deszyfruje treść lokalnie. Całość odbywa się bez konta, bez plików cookie i bez dostępu do bazy IndexedDB.
  • Na Twoich urządzeniach (podgląd): aplikacja pobiera szyfrogram i zawinięty klucz, odpakowuje go lokalnie Twoim kluczem głównym i pokazuje podgląd. Nie odpytuje przy tym publicznego endpointu udostępnień, więc Twój własny podgląd nie zużywa limitu otwarć.

Część 2: Szczegóły techniczne

Sekcja dla osób zainteresowanych kryptografią i architekturą systemową.

Format URL

https://reapps.eu/notes/s/<slug>#k=<base64url_klucz>&v=1
https://reapps.eu/task/s/<slug>#k=<base64url_klucz>&v=1
  • slug to 16 znaków base64url (96 bitów entropii z crypto.getRandomValues). Enumeracja brute-force jest statystycznie niemożliwa i powstrzymywana przez rate limit.
  • k to klucz AES-GCM 256-bit (32 surowe bajty, 43 znaki base64url bez paddingu).
  • v to wersja payloadu (obecnie 1). Gwarantuje to wsteczną kompatybilność przyszłych formatów zaszyfrowanego JSON-a.

Zgodnie z RFC 3986 §3.5 fragment URL (po #) istnieje wyłącznie po stronie klienta. Nie trafia on do logów dostępów (access logs), nie widzi go Service Worker ani serwer (np. Nginx).

Kryptografia pod maską

  • Klucz jednorazowy to wynik wywołania Web Crypto API: crypto.getRandomValues(new Uint8Array(32)).
  • Dane (payload) są szyfrowane AES-GCM z unikalnym wektorem inicjującym (IV) dla każdej operacji. Format iv:ciphertext (base64) pozwala globalnej regexowej funkcji Encryption Guard zapobiegać jakimkolwiek przypadkowym wyciekom tekstu jawnego z aplikacji.
  • Klucz główny właściciela służy jedynie do operacji lokalnego owinięcia (key wrapping) jednorazowego klucza udostępniania i nigdy nie opuszcza urządzenia.

Co dokładnie przechowuje serwer

KolumnaTekst jawny?Przeznaczenie
slugTakPubliczny identyfikator trasy
user_idTakKlucz obcy (FK) do listy zarządzania
snapshot_typeTakOkreśla aplikację (note / task), by nie mieszać udostępnień w UI
created_at, expires_at, revoked_atTakZnaczniki czasowe do sprzątania bazy i logiki interfejsu
access_count, max_access_countTakOperacyjny licznik otwarć
password_hashTak (Argon2id)Weryfikacja hasła przez serwer
payload_encryptedNieGłówny szyfrogram AES-GCM
owner_key_wrappedNieSzyfrogram klucza owinięty kluczem głównym konta
Oryginalny tytuł, treść, podzadania, source_idNieWszystko znajduje się wewnątrz payload_encrypted

Nawet pole source_id powiązujące udostępnienie z konkretną notatką znajduje się w zaszyfrowanym ładunku, uniemożliwiając serwerowi profilowanie, które notatki najczęściej udostępniasz.

Defense-in-depth: minimalizacja danych (payloadu)

Podczas szyfrowania migawki, celowo pomijane są liczne metadane, nawet jeśli byłyby one zaszyfrowane:

  • Z notatek usuwane są flagi is_starred, is_pinned, tags oraz daty aktualizacji.
  • Z zadań wycinane są przypomnienia (reminder_date), zasady powtarzalności i flagi powiadomień.
  • Schematy biblioteki Zod używane podczas deszyfrowania automatycznie usuwają nieznane pola. Odbiorca ma dostęp wyłącznie do tego, co niezbędne do podglądu dokumentu, a nie do metadanych organizacyjnych Twojego systemu pracy.

Atomowe egzekwowanie limitu otwarć (“max otwarć”)

Zablokowanie dostępu po równej liczbie N otwarć rodzi problem zjawiska wyścigu (race condition). W tradycyjnym wariancie dwa równoczesne zapytania mogłyby obejść limit.

Wykorzystywane jest do tego pojedyncze zapytanie do bazy PostgreSQL: UPDATE ... WHERE access_count < max_access_count AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > NOW()) RETURNING .... Instrukcja ta, wykonując się, blokuje dany wiersz. Zwraca zaszyfrowane dane albo błąd 410 Gone (rozróżniający kody EXHAUSTED, EXPIRED, REVOKED). Zapytanie to aktualizuje pole revoked_at = NOW() przy ostatnim udanym pobraniu danych, co uruchamia standardowy proces sprzątania.

Próby podania błędnego hasła są odrzucane jeszcze przed tym etapem, chroniąc Twój limit otwarć przed zmarnowaniem.

Prawdziwa anonimowość widzów

Odwiedzający pod adresem /s/<slug> nie loguje się. Podczas projektowania tego widoku, zadbano o całkowity brak stanu klienta:

  • Service Worker pomija trasy /s/* i /api/shares/* przed jakąkolwiek logiką cache.
  • Komponent hooks.client.ts pomija inicjalizację pamięci masowej (storage) i nasłuchiwaczy sesji dla tych ścieżek.
  • Język interfejsu ustala się w trybie persistPreference: false (żadne dane o preferencjach nie są zapisywane w localStorage).
  • Żądanie fetch omijające autoryzację działa w trybie credentials: 'omit' (ignoruje istniejące pliki cookie sesji).

Otwierając link w trybie incognito i zamykając go, po użytkowniku zostaje ewentualnie wpis w historii przeglądarki.

Ochrona odbiorcy

Anti-phishing: Ekran monitujący o hasło oraz ekrany błędów są renderowane przez wystandaryzowany komponent ShareGate. Błędne strony zawierają spójne wizualnie informacje prowadzące bezpośrednio do reapps.eu (lub odpowiedniej domeny w przypadku własnego hostingu).

Sanityzacja Markdown: Każda udostępniona treść zamieniana z Markdown na HTML przechodzi przez proces marked, a następnie bibliotekę DOMPurify z rygorystyczną listą dozwolonych wyjątków (allowlist). Usuwane są formaty takie jak javascript: czy data:. Dodatkową ochronę stanowi architektura oparta na CSP (Content Security Policy) wykorzystująca unikalne tokeny (nonce).

Blokada automatycznego ładowania obrazków: Widok migawki domyślnie blokuje zewnętrzne grafiki (“zapytaj przed załadowaniem”), co zapobiega wyciekowi adresów IP odbiorców do zewnętrznych serwerów hostujących multimedia.

Kontrola ruchu (Rate limits)

  • POST /api/shares (tworzenie): 30 zapytań na godzinę per user (zabezpieczenie przed nadużyciem hostingu).
  • GET /api/shares/<slug> (odczyt bez hasła): 100 na minutę per IP (obsługa np. linków wiralowych z newslettera).
  • Błędne hasło udostępnienia: dedykowany limit 10 prób na 15 minut (nie wpływa na Twój główny limit prób logowania do konta wewnątrz tej samej sieci np. uczelnianej).

Cykl życia plików

  • Unieważnienie (revoke) działa jako tzw. miękkie usunięcie (soft delete). Szyfrogram pozostaje w bazie przez 24 godziny (przestrzeń na błędy “usunąłem przez przypadek”), po czym następuje ostateczne, trwałe usunięcie (hard delete) wykonywane przez zadanie w tle.
  • Zamiast obciążających harmonogramów (tzw. zadań cron), proces sprzątania jest “leniwy” – każde zapytanie HTTP wywołane do serwera ma 0.5% szansy na wyzwolenie procedury usuwania starych wpisów.

Pełna implementacja – włącznie z modelem zagrożeń i audytem bezpieczeństwa – znajduje się w repozytorium projektu. Aby przetestować tę funkcję, zaloguj się na reapps.eu i wybierz przycisk “Udostępnij” w dowolnej notatce lub zadaniu.

Nie znasz jeszcze Reborn Apps? Wypróbuj aplikację na publicznej instancji - darmowej, zero-knowledge, do której założenia nie wymagasz maila. Albo postaw własną.