Przejdź do treści
WyślijBezpiecznie
Jak to działa Cennik Bezpieczeństwo Sprawdź plik Kontakt
Zaloguj Załóż konto

← Jak działa szyfrowanie

Format kryptograficzny WB1

Techniczna specyfikacja szyfrowania używanego w WyślijBezpiecznie, dla osób, które chcą sprawdzić konstrukcję albo napisać zgodną implementację. Wektory testowe: wektory.json.

Spis treści
  1. Założenia
  2. Prymitywy
  3. Klucze
  4. Manifest
  5. Plik: szyfrowanie strumieniowe
  6. Skrót paczki (packageSha256)
  7. Dostarczenie linku
  8. Prośba o pliki
  9. Czego nie chroni WB1

Wersja 1. Ten dokument jest źródłem prawdy dla każdej implementacji (przeglądarka, klient Windows, narzędzia). Implementacja jest zgodna z WB1 wtedy, gdy przechodzi wszystkie wektory testowe z pliku wb1.json, w tym negatywne (w repozytorium docs/test-vectors/wb1.json, publicznie: wyslijbezpiecznie.pl/bezpieczenstwo/wb1/wektory.json).

Implementacja referencyjna: biblioteka @wb/crypto (TypeScript, WebCrypto), ta sama w przeglądarce i w programie dla Windows. Testy sprawdzają wektory tą implementacją oraz niezależnie modułem node:crypto.

Założenia

  • Szyfrowanie i deszyfrowanie odbywa się wyłącznie u klientów. Serwer przechowuje szyfrogram i materiał, z którego sam nie odtworzy klucza.
  • Jedna wysyłka (paczka) = jeden losowy klucz treści CEK. Pliki szyfrujemy raz, niezależnie od liczby odbiorców.
  • Każdy odbiorca ma własny klucz KEK z dwóch części: A (fragment linku z maila; serwer nigdy jej nie zapisuje, a w trybie „wyślę link sam” w ogóle jej nie dostaje, patrz „Dostarczenie linku”) i B (na serwerze, wydawana po poprawnym kodzie). Sam kod nie jest częścią klucza.

Prymitywy

Nazwa Definicja
HKDF(ikm, salt, info) HKDF-SHA-256 (RFC 5869), wynik 32 bajty. Pusta sól = ciąg zerowej długości. info to tekst UTF-8.
GCM(k, nonce, pt, aad) AES-256-GCM, nonce 12 bajtów, znacznik 16 bajtów doklejony na końcu szyfrogramu.
rand(n) n bajtów z kryptograficznego generatora (WebCrypto getRandomValues).
b64u(x) base64url bez dopełnienia = (RFC 4648 §5).
u64be(x) liczba jako 8 bajtów big-endian.

Klucze

CEK         = rand(32)                                        jeden na wysyłkę
A_i, B_i    = rand(32), rand(32)                              dla każdego odbiorcy i
kekSalt_i   = rand(16)                                        jawna, przechowywana na serwerze
KEK_i       = HKDF(A_i ‖ B_i, kekSalt_i, "WB1 recipient kek")
MK          = HKDF(CEK, "", "WB1 manifest")                   klucz manifestu
FK_f        = HKDF(CEK, fileSalt_f, "WB1 file")               klucz pliku f

Owinięcie CEK dla odbiorcy

wrapNonce_i = rand(12)
wrapped_i   = wrapNonce_i ‖ GCM(KEK_i, wrapNonce_i, CEK, "WB1 wrap:" ‖ recipientId_i)      60 bajtów

recipientId_i to publiczny identyfikator odbiorcy z linku (tekst ASCII). Wiąże owinięty klucz z odbiorcą: nie da się go podstawić innemu odbiorcy.

Serwer przechowuje: B_i (dodatkowo zaszyfrowane kluczem serwera), kekSalt_i, wrapped_i. Link w mailu: https://wyslijbezpiecznie.pl/o/<recipientId>#<b64u(A_i)>. Przeglądarka nie wysyła fragmentu #… na serwer.

Manifest

Manifest opisuje paczkę i jest szyfrowany, bo nazwy plików często zawierają dane osobowe.

{
  "v": 1,
  "message": "opcjonalna wiadomość nadawcy",
  "files": [
    { "index": 0, "path": "PIT-11/Kowalski.pdf", "size": 12345, "type": "application/pdf", "sha256": "<hex>" }
  ]
}
  • path to ścieżka względna z / jako separatorem (zachowuje strukturę folderów). Nie może zaczynać się od /, zawierać .. ani znaków sterujących.
  • index odpowiada kolejności plików w wysyłce.
  • sha256 to SHA-256 tekstu jawnego pliku (hex, małe litery).
  • Paczka przesłana w odpowiedzi na prośbę o pliki (patrz „Prośba o pliki”) ma dodatkowo items (kopia listy dokumentów z prośby, najwyżej 20 pozycji po 200 znaków) i przy plikach item: numer pozycji z items, do której klient dołączył plik. Plik bez item to „inne pliki”. Może też mieć required (kopia oznaczeń z prośby) i skipped: listę { "item": n, "reason": "…" } z obowiązkowymi pozycjami, które klient pominął z wyjaśnieniem (najwyżej 500 znaków); pominięta pozycja nie ma plików.
manifestNonce = rand(12)
manifestCt    = manifestNonce ‖ GCM(MK, manifestNonce, UTF8(JSON), "WB1 manifest:" ‖ transferId)

Plik: szyfrowanie strumieniowe

Nagłówek (28 bajtów)

Bajty Pole Wartość
0–3 magic ASCII WB1F
4 wersja 1
5 algorytm 1 = AES-256-GCM
6–7 zarezerwowane 0x0000
8–11 chunkSize rozmiar kawałka tekstu jawnego, u32 big-endian (domyślnie 4 MiB = 4194304)
12–27 fileSalt rand(16)

Kawałki

Tekst jawny długości L dzielimy na n = max(1, ceil(L / chunkSize)) kawałków po chunkSize bajtów. Ostatni może być krótszy, a pusty plik to jeden pusty kawałek.

nonce_k = 0x000000 ‖ u64be(k) ‖ final            11 bajtów licznika + 1 bajt: 0x01 dla ostatniego kawałka, 0x00 dla pozostałych
ct_k    = GCM(FK_f, nonce_k, pt_k, header)       AAD = cały 28-bajtowy nagłówek

To konstrukcja STREAM (jak w age):

  • zamiana kolejności kawałków zmienia licznik w nonce;
  • obcięcie pliku zostawia jako ostatni kawałek zaszyfrowany z final = 0;
  • doklejenie kawałka po ostatnim jest wykrywane;
  • zmiana nagłówka (np. chunkSize) psuje AAD.

Każdy z tych przypadków kończy się błędem uwierzytelnienia. Klucz FK_f jest unikalny dla każdego pliku (losowa sól), więc licznik jako nonce jest bezpieczny.

Postać kanoniczna zaszyfrowanego pliku (używana w wektorach): header ‖ ct_0 ‖ ct_1 ‖ … ‖ ct_{n-1}. API przenosi nagłówek jako metadane pliku, a kawałki osobno.

Odbiorca po odszyfrowaniu liczy SHA-256 tekstu jawnego i porównuje go z sha256 z manifestu.

Skrót paczki (packageSha256)

Potwierdza kompletność paczki: żaden plik nie został dodany, usunięty ani podmieniony.

lista  = pliki posortowane rosnąco po (sha256 jako bajty, potem size)
packageSha256 = SHA-256( "WB1-PKG" ‖ Σ ( sha256_f[32 bajty] ‖ u64be(size_f) ) )        hex

Nazwy plików nie wchodzą do skrótu, więc ten sam komplet plików daje ten sam skrót niezależnie od nazw. Serwer zna sha256 i size każdego pliku (potrzebne do metryki), ale nie zna nazw ani treści.

Dostarczenie linku

  • Domyślnie mail z linkiem wysyła platforma. Klient przekazuje część A serwerowi wyłącznie na czas wysłania maila: A jest używana w pamięci procesu, nie jest zapisywana w bazie, kolejce, logach ani kopiach zapasowych. Wyciek bazy, magazynu plików lub kopii nie ujawnia A.
  • Opcja „wyślę link sam”: klient nie przekazuje A. Nadawca wysyła link ze swojej poczty (gotowa treść do skopiowania lub otwarcia w programie pocztowym). Serwer nigdy nie widzi A.
  • Kod dostępu zawsze generuje i dostarcza platforma (SMS, drugi e-mail) albo pokazuje go nadawcy (tryb ręczny). Kod nie jest częścią klucza.

Prośba o pliki

Zamawiający (użytkownik z kontem) prosi klienta (bez konta) o dokumenty. Dwa kroki, oba w tym samym formacie:

1. Treść prośby (wiadomość i lista dokumentów) jest szyfrowana w przeglądarce zamawiającego kluczem prośby RK:

RA, RB      = rand(32), rand(32)
reqSalt     = rand(16)                                         jawna, przechowywana na serwerze
RK          = HKDF(RA ‖ RB, reqSalt, "WB1 request key")
contentCt   = nonce ‖ GCM(RK, nonce, UTF8(JSON), "WB1 request:" ‖ requestId)       nonce = rand(12)
{ "v": 1, "message": "opcjonalna wiadomość", "items": ["PIT-11 od pracodawcy", "Zaświadczenie o dochodach"], "required": [true, false] }

required[i] oznacza, że pozycja items[i] jest obowiązkowa (tablica tej samej długości co items). Bez pola wszystkie pozycje są opcjonalne.

requestId to publiczny identyfikator prośby z linku (q_ + 16 znaków base64url). Link do klienta: https://wyslijbezpiecznie.pl/przeslij/<requestId>#<b64u(RA)>. Serwer przechowuje RB (zaszyfrowane kluczem serwera), reqSalt i contentCt; RB wydaje po poprawnym kodzie SMS (gdy zamawiający go włączył) albo bez kodu. Link wysyła platforma (RA tylko w pamięci, jak przy zwykłej wysyłce) albo zamawiający ze swojej poczty (serwer nie zna RA).

2. Pliki od klienta to zwykła paczka WB1 tworzona w przeglądarce klienta: nowy CEK, pliki, manifest z items i item, jeden odbiorca: zamawiający. Klient losuje dla niego A, B i kekSalt i owija CEK jak dla każdego odbiorcy. Serwer przechowuje B, a A dostaje wyłącznie na czas wysłania zamawiającemu maila z linkiem https://wyslijbezpiecznie.pl/app/prosby/pliki/<recipientId>#<b64u(A)>. Zamiast kodu B wydawane jest tylko zalogowanemu zamawiającemu (konto z hasłem i, jeśli włączona, weryfikacją dwuetapową).

Zabezpieczenia są takie jak przy zwykłej wysyłce: do treści prośby potrzebny jest link klienta (RA) i serwer (RB, po kodzie), do plików link z maila zamawiającego (A) i serwer (B, po zalogowaniu na konto zamawiającego). Link do klienta nie daje dostępu do przesłanych plików.

Czego nie chroni WB1

  • Atakujący, który ma jednocześnie link z maila (A) i dostęp do serwera (B), odszyfruje paczkę. To samo dotyczy kogoś, kto przejmie działający serwer w chwili wysyłki maila przez platformę albo podmieni kod strony odbiorcy; przed tym nie chroni żadna aplikacja webowa. Ochronę daje rozdzielenie kanałów oraz to, że B jest wydawane dopiero po kodzie, z limitem prób.
  • Firmowe filtry poczty (np. Microsoft Safe Links, Proofpoint, Mimecast) przepisują linki razem z fragmentem #…, więc część A może trafić do dostawcy poczty odbiorcy. Bez kodu i B nie wystarcza do odszyfrowania.
  • Kod jest krótki (6 cyfr), bo nie jest częścią klucza: nie da się go sprawdzać offline, a serwer wydaje B dopiero po poprawnym kodzie, z limitem 5 prób na odbiorcę (potem blokada i powiadomienie nadawcy) i limitem prób z jednego adresu IP. Kody SMS i e-mail serwer przechowuje jako HMAC; kod do podyktowania (tryb ręczny) jest przechowywany zaszyfrowany kluczem serwera, żeby nadawca mógł go odczytać w historii.
  • Ktoś, kto ma link, może celowo wyczerpać limit prób i zablokować odbiorcę; nadawca wysyła wtedy nowy kod.
  • Strona odbiorcy po odczytaniu usuwa fragment #A z paska adresu i historii przeglądarki (na czas karty trzyma go w sessionStorage).
  • Metadane jawne dla serwera to: liczba plików, ich rozmiary (z dokładnością do kawałka), skróty SHA-256 tekstu jawnego, czas, nadawca i odbiorcy.
WyślijBezpiecznie

Bezpieczne wysyłanie dokumentów z danymi osobowymi. Przesyłki otwieraj i sprawdzaj wyłącznie na domenie wyslijbezpiecznie.pl.

Usługodawca
SOFTART Krzysztof Michałowski
ul. Pomorska 16D/11, 80-333 Gdańsk
NIP 5841182350
kontakt@wyslijbezpiecznie.pl
+48 601 654 072
Incydenty i podatności: bezpieczenstwo@wyslijbezpiecznie.pl
Dokumenty
  • Bezpieczeństwo dla działów IT
  • Jak działa szyfrowanie
  • Regulamin
  • Polityka prywatności
  • Umowa powierzenia (wzór)
  • Deklaracja dostępności
  • Sprawdź plik lub metrykę
© 2026 SOFTART Krzysztof Michałowski