Architektura PrivMX – klient

Informacje wstępne

Moduł kliencki to centralna część systemów opartych o architekturę PrivMX. Serwer oferuje ograniczoną funkcjonalność i domyślnie to aplikacja kliencka realizuje całość logiki biznesowej systemu dbając jednocześnie o odpowiednie szyfrowanie danych.

Standardowa biblioteka kliencka PrivMX dostarcza podstawowych elementów do budowania logiki systemu w oparciu o:

  • mechanizmy bezpiecznego tworzenia kont i logowania;
  • obiekty „wysokopoziomowe” takie, jak katalogi, pliki, skrzynki nadawcze, odbiorcze itd.;
  • udostępnianie danych;
  • szyfrowanie danych przed wysłaniem na serwer, połączone z odpowiednim generowaniem i przechowywaniem kluczy.

W tym rozdziale opisujemy szkicowo powyższe funkcje.

Rozszerzone klucze ECC

Standardowy moduł kliencki PrivMX wykorzystuje tzw. rozszerzone klucze ECC (p. specyfikacja BIP 32) – są to zwykłe klucze privkey 256bit i pubkey 512bit (spakowane do 257bit) rozbudowane o dodatkową daną, tzw chaincode o długości 256 bitów – „dodatkowe źródło entropii”. Klucze rozszerzone zapisujemy w tym dokumencie z plusem – jako privkey+chaincode oraz pubkey+chaincode.

Chaincodes wykorzystywane są przez klienta PrivMX w procesie derywowania kluczy pochodnych z kluczy istniejących (funkcja CKD z BIP 32) oraz jako klucze do szyfrowania danych użytkownika algorytmem symetrycznym (AES).

Tworzenie kont

Protokół PrivMX TLS pozwala aplikacjom klienckim na logowanie, tzn. wymuszenie przeprowadzenia dodatkowej procedury handshake opartej na SRP. Żeby było to możliwe, serwer musi być w posiadaniu weryfikatora SRP. Zapewnienie tego jest jednym z celów procedury tworzenia konta. Innym celem tej procedury jest ustalenie i zapamiętanie klucza prywatnego i publicznego dla nowego użytkownika.

Nowe konta użytkowników na serwerze PrivMX domyślnie tworzone mogą być przez programy klienckie jedynie na bazie „zaproszeń” – 32-bajtowych, losowych tokenów. Do generowania tokenów służy metoda API.generateNewUserToken dostępna w połączeniu zalogowanym dla wyróżnionych użytkowników („administratorów”). Pierwszy token dla pierwszego użytkownika generowany jest zazwyczaj podczas procedury instalacji serwera PrivMX.

Dowolny, połączony standardowo program kliencki może rozpocząć procedurę tworzenia konta, o ile posiada ważny token oraz poprosił już użytkownika o podanie jego nazwy i hasła.

  1. Program kliencki losuje Sól (16 bajtów) oraz IlośćRund (liczba z przedziału 4000-5000);
  2. wyznacza MixedPassword = H( hasło użytkownika, Sól, IlośćRund ), gdzie H to ustalony przez program kliencki algorytm hashowania, np. PBKDF2-SHA512;
  3. oblicza srpVerifier wykorzystując wartość hash = SHA512( MixedPassword ) % 16 bajtów;
  4. losuje MasterKey = rozszerzony prywatny klucz ECC;
  5. ustala privData = AES256Encrypt( MasterKey ) z hasłem równym SHA256( MixedPassword );
  6. oblicza identityKey = CKD( MasterKey, m/0′ ), a następnie identityKeyPub = rozszerzony klucz publiczny liczony dla identityKey;
  7. wywołuje metodę API.register, do której przekazuje:
    • token otrzymany od administratora;
    • nazwę nowego użytkownika;
    • srpVerifier – będzie wykorzystywany przez serwer podczas późniejszego logowania;
    • Sól, IlośćRund i nazwę algorytmu hashowania – serwer dostarczy te dane każdemu klientowi, który będzie chciał się zalogować na to konto;
    • privData, które klient będzie mógł bezpiecznie pobrać po zalogowaniu (p.poniżej);
    • identityKeyPub – klucz publiczny nowego użytkownika, który będzie opublikowany przez serwer w ramach usługi PrivMX PKI;
    • signature – podpis zestawu wszystkich powyższych danych wykonany kluczem prywatnym identityKey.

Serwer zapamiętuje powyższe dane i korzysta z nich przy późniejszych próbach logowania na nowo utworzone konto.

Logowanie i inicjalizacja klienta

Aplikacja kliencka korzystająca z modułu PrivMX swoje działanie rozpoczyna najczęściej od uzyskania standardowego połączenia z serwerem PrivMX. W tym celu łączy się z ustalonym endpointem API lub wykorzystuje najpierw procedurę Service Discovery dla podanego hostname lub adresu user#hostname.

Jeśli aplikacja chce uzyskać dostęp do danych określonego użytkownika lub chce wysyłać w jego imieniu wiadomości – musi przeprowadzić procedurę logowania. Zakładając, że klient nawiązał już standardowe połączenie PrivMX TLS i posiada podane przez użytkownika jego identyfikator i hasło:

  1. klient pobiera z serwera Sól i IlośćRund zapisane dla danej nazwy użytkownika (API.getLoginParams);
  2. korzystając z tych danych oraz hasła użytkownika oblicza MixedPassword oraz srpVerifier – tak samo jak podczas zakładania konta;
  3. wymusza odpowiednią funkcją PrivMX TLS przeprowadzenie hadshake SRP, w wyniku czego zmieniają się klucze szyfrowania połączenia oraz uzyskuje autoryzację jako użytkownik posiadający konto – otrzymuje dostęp do metod API dla zalogowanych użytkowników;
  4. pobiera z serwera privData użytkownika (API.getPrivData);
  5. oblicza MasterKey = AES256Decrypt( privData ) z hasłem równym SHA256( MixedPassword );
  6. oblicza identityKey oraz identityKeyPub – na bazie MasterKey, w taki sam sposób jak podczas zakładania konta.

Dwa klucze uzyskane w wyniku przeprowadzenia powyższej procedury stanowią podstawę do dalszego działania standardowego modułu klienckiego:

  • rozszerzony klucz prywatny MasterKey stanowi dla programu klienckiego „główny uchwyt” do danych użytkownika zapisanych na serwerze – generowane są z niego:
    • rozszerzony klucz prywatny HomeDir = CKD( MasterKey,m/1′ ) wyznaczający i dający pełen dostęp do „katalogu domowego” użytkownika;
    • rozszerzony klucz prywatny SinkList = CKD( MasterKey,m/2′ ) wyznaczający i dający pełen dostęp do pliku przechowującego klucze prywatne do skrzynek pocztowych użytkownika.
  • rozszerzony klucz prywatny IdentityKey = CKD( MasterKey, m/0′ ) to klucz prywatny użytkownika, który wykorzystywany jest do odszyfrowywania wiadomości skierowanych do użytkownika, do potwierdzania jego tożsamości (podpisy) itp.

Wszystkie wyżej wymienione klucze generowane są przez program kliencki i nie są przechowywane po stronie serwera.

Po przeprowadzeniu pierwszego logowania standardowy moduł kliencki nie może odczytać katalogu HomeDir oraz pliku SinkList, ponieważ one jeszcze nie istnieją. Musi je utworzyć i może wypełnić je danymi początkowymi zależnie od aplikacji. Domyślne działanie jest takie, że tworzony jest wówczas pusty katalog domowy oraz pierwsza skrzynka pocztowa, której klucz prywatny umieszczany jest w pliku SinkList. Klucz publiczny skrzynki może być ustalony jako domyślny publiczny SID użytkownika powiązany z adresem nazwa#hostname.

Pliki i katalogi

Serwer PrivMX nie oferuje funkcji API związanych z obiektami takimi jak pliki, czy katalogi. Jeżeli aplikacja kliencka chce wykorzystywać takie obiekty i przechowywać je na serwerze PrivMX, to musi je symulować przy pomocy dostępnych metod API. Standardowy moduł kliencki robi to przyjmując następujące założenia:

  • plik to deskryptor z powiązanymi blokami. Dane pliku przechowywane są w blokach, a metadane pliku w polu Extra deskryptora.
  • Katalog to specjalny plik (w formacie json) zawierający listę nazw i kluczy plików oraz podkatalogów, które w danym katalogu mają się znajdować. Pole Extra deskryptora zawiera metadane katalogu.

Klucz HomeDirKey uzyskiwany po logowaniu to rozszerzony klucz prywatny katalogu głównego, którego bloki zawierają listę wszystkich plików i katalogów "głównego poziomu" – ich identyfikatory, klucze publiczne i prywatne. W ten sposób klient PrivMX uzyskuje strukturę drzewiastą zaszyfrowanych plików i katalogów.

Struktura ta oraz dane plików są ukryte przed serwerem, ponieważ standardowy moduł kliencki PrivMX szyfruje dane, które umieszcza w blokach oraz w polach Extra deskryptorów. Stosuje w tym celu szyfrowanie symetryczne – algorytm AES256 – jako klucze stosując chaincodes z rozszerzonych kluczy ECC.

Przykładowo: aby zapisać plik w katalogu k, program kliencki musi posiadać rozszerzony klucz prywatny tego katalogu (kpriv+kchaincode), żeby udowodnić serwerowi, że ma prawa do jego zapisu/modyfikacji. Wykonuje wówczas procedurę tworzenia nowego deskryptora z odpowiednio zaszyfrowanymi danymi oraz dopisania go do katalogu k:

  1. Losuje 256-bitowy klucz KS do szyfrowania danych pliku.
  2. Dzieli plik na bloki, szyfruje każdy z nich kluczem KS i nadaje im BID (hash szyfrogramu).
  3. Losuje nowy rozszerzony klucz prywatny (dpriv+dchaincode) i generuje z niego rozszerzony klucz publiczny (dpub+dchaincode) – identyfikator nowego pliku.
  4. Przygotowuje strukturę Metadata zawierającą:
    • type = „file”
    • data = { nazwapliku, mimetype, rozmiar, data utworzenia i modyfikacji pliku }
    • blockskey = KS
  5. Tworzy na serwerze nowy deskryptor (p. poprzednie rozdziały) przesyłając przygotowane bloki, klucz dpub, odpowiedni DID oraz:
    • Extra = AES256Encrypt( data=Metadata, key=dchaincode )
    • Podpis całości żądania wykonany kluczem dpriv
  6. Korzystając z klucza prywatnego kpriv modyfikuje katalog/deskryptor k tak, że dopisuje do jego danych (json) linię-czwórkę zawierającą:
    • name = nazwapliku
    • type = ”file”
    • pub = ( dpub+dchaincode )
    • encpriv = AES256Encrypt( data=dpriv, key=kpriv )

Możliwość odczytania danych związanych z deskryptorem pliku/katalogu zależna jest od kluczy, które posiada program kliencki:

  • Posiadanie samego DID nie umożliwia w zasadzie dostępu do danych. Pozwala sprawdzić, czy dany deskryptor istnieje i dzięki dostępowi do pola Blocks pozwala pobrać bloki zaszyfrowanych danych.
  • Posiadanie rozszerzonego klucza publicznego (dpub+dchaincode) pozwala na powyższe oraz dodatkowo na odszyfrowanie pola Extra, czyli uzyskanie metadanych pliku oraz klucza blockskey. Dzięki temu ostatniemu klient może odszyfrować bloki danych – otrzyma wówczas odszyfrowane dane pliku. W przypadku katalogu otrzyma listę plików i podkatalogów, które się w nim znajdują i tym samym możliwość czytania katalogu „wgłąb”.
  • Posiadanie rozszerzonego klucza prywatnego (dpriv+dchaincode) pozwala odczytać wszystkie dane jw. oraz dodatkowo umożliwia programowi klienckiemu modyfikację deskryptora i jego usunięcie. W przypadku gdy jest to katalog – możliwe jest rozszyfrowanie kluczy prywatnych plików i podkatalogów oraz w konsekwencji ich modyfikacja i usuwanie.

Wysyłanie i odbieranie wiadomości

Po przeprowadzeniu procedury logowania standardowa biblioteka kliencka PrivMX uzyskuje klucz SinkList do pliku zawierającego listę skrzynek pocztowych użytkownika. Dla każdej skrzynki znaleźć tu można jej nazwę, opis oraz rozszerzony klucz prywatny. Plik ten jest odpowiednio aktualizowany po utworzeniu nowej lub usunięciu niepotrzebnej skrzynki pocztowej.

Program kliencki musi znać identyfikator skrzynki (SID) adresata, aby wysłać do niego wiadomość. Aplikacja kliencka, w zależności od wymagań może dbać na różne sposoby o to, żeby użytkownicy znali SID swoich skrzynek pocztowych.

Domyślnie zakłada się zdecentralizowaną konfigurację systemu opartego o architekturę PrivMX i dlatego standardowy moduł kliencki po pierwszym logowaniu tworzy skrzynkę pocztową dla użytkownika, dopisuje ją do SinkList i jej identyfikator umieszcza w publicznym rekordzie użytkownika w usłudze PrivMX PKI. Przy pomocy tej usługi nadawcy wiadomości mogą pobrać (i zweryfikować!) domyślny SID użytkownika. Więcej na ten temat przeczytać można w ostatnim rozdziale dokumentu.

Standardowy moduł kliencki wykorzystuje API.messagePut do pozostawienia swej wiadomości w odpowiedniej skrzynce na docelowym serwerze. Wykorzystuje jednak w specyficzny sposób bloki wiadomości i pole Extra:

  • Bloki danych wykorzystywane są głównie do przesyłania plików – np. załączników do wiadomości. Dla każdego załącznika losowany jest nowy klucz, którym szyfrowane są bloki tego załącznika (AES256Encrypt).
  • Główna struktura danych wiadomości umieszczana jest w polu Extra i szyfrowana jest wspólnym sekretem generowanym zgodnie ze schematem ECIES dla klucza prywatnego IdentityKey nadawcy i klucza publicznego SID skrzynki. Standardowa struktura wiadomości zawiera, między innymi:
    • tytuł i treść wiadomości – pola tekstowe formatowane dowolnie przez klienta;
    • senderName – nazwa nadawcy (np. „John Smith”)
    • attachments – lista załączników; dla każdego określone są:
      • nazwa i mimetype;
      • blocks – BIDs bloków tego załącznika;
      • key – klucz szyfrujący te bloki.

Zastosowanie w schemacie ECIES klucza publicznego skrzynki pocztowej (SID) powoduje, że treść przesyłanych danych będzie mogła być pobrana, rozszyfrowana i odczytana tylko poprzez posiadacza klucza prywatnego docelowej skrzynki pocztowej. Jeśli jest to np. skrzynka domyślna użytkownika, który nie dzielił się z nikim swoimi kluczami (co jest najczęstszym przypadkiem), to wiadomość odczyta tylko ten użytkownik.

Udostępnianie danych

Standardowy sposób udostępniania danych w ramach architektury PrivMX polega na przekazywaniu odpowiednich kluczy do określonych obiektów przechowywanych przez serwer. Chcąc np. udostępnić innemu użytkownikowi plik tylko do odczytu – należy przekazać mu klucz publiczny do niego.

Z klucza prywatnego ECC oblicza się odpowiedni klucz publiczny, zatem na przykład przekazując klucz prywatny do deskryptora, dajemy odbiorcy możliwość jego modyfikacji, ale też i odczytu. W przypadku skrzynek pocztowych otrzymanie prawa do odczytu (klucz prywatny) daje zarazem prawo do umieszczania w niej nowych wiadomości.

Sposób dystrybucji kluczy zależy w pełni od aplikacji klienckiej, jej przeznaczenia, sposobu działania i rozwiązań interfejsu użytkownika. Klucze do wspólnych obiektów (najczęściej plików i katalogów) rozsyłane są najczęściej wiadomościami do domyślnych lub specjalnych skrzynek użytkowników. Takie podejście umożliwia również udostępnianie danych pomiędzy użytkownikami z różnych serwerów, aczkolwiek warunkiem jest tu odpowiednia konfiguracja serwerów dopuszczająca dostęp do danych również „obcym użytkownikom”.

Uwagi nt. implementacji i modyfikacji klienta

Wszystkie klucze wykorzystywane przez system oparty na architekturze PrivMX są losowane przez moduł kliencki, dlatego jednym z najważniejszych elementów wpływającym bezpieczeństwo takiego systemu jest wysoka jakość generatora liczb losowych. Każda implementacja modułu klienckiego PrivMX powinna o to zadbać.

Procedury zakładania konta i logowania mogą być rozszerzone o wykorzystanie schematu 2-factor authentication.

Grupowanie żądań to optymalizacja protokołu PrivMX TLS, która usprawnia przeprowadzanie operacji na plikach i katalogach wymagających dokonania wielu zapytań API. Oczywiście optymalizacja taka wymaga również wsparcia po stronie serwera.

Derywowany domyślnie po zalogowaniu klucz SinkList do pliku zawierającego listę prywatnych skrzynek może być zastąpiony zwykłym plikiem o ustalonej nazwie w katalogu HomeDir.

Możliwe jest również przechowywanie kluczy prywatnych skrzynek pocztowych w nich samych – w ich polu Extra. Może to być przydatna optymalizacja, gdy API serwera rozszerzone jest o funkcje listujące wszystkie skrzynki danego użytkownika. Klucz prywatny skrzynki powinien być wówczas zaszyfrowany chainkodem klucza HomeDir (na przykład).

Domyślnie, poprzez procedurę derywacji kluczy CKD uzyskiwane są tylko „główne mountpointy” dla użytkownika, ale aplikacja kliencka może w ten sposób tworzyć na własne potrzeby dowolne, większe hierarchie kluczy prywatnych.

Programy klienckie mogą tworzyć wiadomości, które są możliwe do odszyfrowania tylko przez określonych użytkowników (a nie przez dowolnego właściciela klucza prywatnego skrzynki). W tym celu w treści wiadomości umieszczać mogą część zaszyfrowaną schematem ECIES z wykorzystaniem klucza prywatnego nadawcy i klucza publicznego odbiorcy (a nie skrzynki).

Parametry konfiguracji serwera, które są istotne dla programów klienckich (np. maksymalna wielkość bloku danych) dostępne są do odczytu metodą API.getServerConfig.


Następny rozdział: PrivMX PKI – prywatna baza kluczy z publiczną historią zmian, pobieranie i weryfikacja kluczy publicznych, audyty, konsensus, web-of-trust, przykład wykorzystania PrivMX PKI.
Spis treści: Architektura PrivMX
Wersja PDF: Architektura PrivMX – opis techniczny (pdf, PL)english version