Podobnie jak w przypadku większości oprogramowania do szyfrowania dysków i plików, szyfrowanie pamięci Androida tradycyjnie polega na nieprzetworzonych kluczach szyfrowania dostępnych w pamięci systemowej, aby można było przeprowadzić szyfrowanie. Nawet jeśli szyfrowanie jest wykonywane przez dedykowany sprzęt, a nie oprogramowanie, to oprogramowanie nadal musi zarządzać nieprzetworzonymi kluczami szyfrowania.
Tradycyjnie nie jest to traktowane jako problem, ponieważ klucze nie będą dostępne podczas ataku offline, przed którym ma być zabezpieczane szyfrowanie pamięci masowej. Chcemy jednak zapewnić lepszą ochronę przed innymi typami ataków, takimi jak ataki z zimnego rozruchu czy ataki online, w których przypadku atakujący może uzyskać dostęp do pamięci systemowej bez całkowitego skompromitowania urządzenia.
Aby rozwiązać ten problem, w Androidzie 11 wprowadzono obsługę kluczy zaszyfrowanych sprzętowo, gdzie dostępna jest obsługa sprzętowa. Klucze zaszyfrowane sprzętowo to klucze magazynowe, które są znane w postaci surowej tylko dla dedykowanego sprzętu. Oprogramowanie widzi i działa z tymi kluczami tylko w zaszyfrowanej formie. Sprzęt musi umożliwiać generowanie i importowanie kluczy magazynu, stosowanie kluczy magazynu w formie tymczasowej i długoterminowej, wyprowadzanie kluczy podrzędnych, bezpośrednie programowanie jednego klucza podrzędnego w ramach wbudowanego mechanizmu szyfrowania oraz zwracanie oddzielnego klucza podrzędnego do oprogramowania.
Uwaga: wbudowany mechanizm szyfrowania (wbudowany sprzęt do szyfrowania) odnosi się do sprzętu, który szyfruje/odszyfrowuje dane w trakcie przesyłania do/z urządzenia pamięci masowej. Zwykle jest to host UFS lub kontroler eMMC, który implementuje rozszerzenia szyfrowania zdefiniowane w odpowiedniej specyfikacji JEDEC.
Projektowanie
W tej sekcji przedstawiono projekt funkcji kluczy w ramach oprogramowania sprzętowego, w tym wymagania dotyczące sprzętu. Ta dyskusja koncentruje się na szyfrowaniu plików (FBE), ale rozwiązanie dotyczy też szyfrowania metadanych.
Jednym ze sposobów uniknięcia potrzeby przechowywania nieprzetworzonych kluczy szyfrowania w pamięci systemowej jest przechowywanie ich tylko w kluczowych slotach wbudowanego modułu szyfrowania. Pojawia się jednak kilka problemów:
- Liczba kluczy szyfrowania może przekroczyć liczbę sekcji kluczy.
- Wbudowane mechanizmy kryptograficzne zwykle tracą zawartość przedziałów kluczy, gdy kontroler pamięci masowej (zwykle UFS lub eMMC) zostanie zresetowany. Resetowanie kontrolera pamięci jest standardową procedurą naprawy błędów, która jest wykonywana w przypadku wystąpienia określonych typów błędów pamięci. Takie błędy mogą wystąpić w dowolnym momencie. Dlatego, gdy używana jest szyfrowanie w ramach, system operacyjny musi być zawsze gotowy do przeprogramowania slotów kluczy bez udziału użytkownika.
- Wbudowane silniki kryptograficzne mogą być używane tylko do szyfrowania/odszyfrowywania pełnych bloków danych na dysku. W przypadku FBE oprogramowanie musi jednak mieć możliwość wykonywania innych operacji kryptograficznych, takich jak szyfrowanie nazw plików i wyodrębnianie identyfikatorów kluczy. Oprogramowanie nadal będzie potrzebować dostępu do nieprzetworzonych kluczy FBE, aby wykonywać te inne czynności.
Aby uniknąć tych problemów, klucze magazynu są zamiast tego tworzone jako klucze opakowane sprzętowo, które mogą być rozpakowane i używane tylko przez dedykowany sprzęt. Umożliwia to obsługę nieograniczonej liczby kluczy. Hierarchia kluczy jest dodatkowo modyfikowana i częściowo przenoszona na ten sprzęt, co umożliwia zwrot klucza podrzędnego do oprogramowania na potrzeby zadań, które nie mogą korzystać z wbudowanego silnika kryptograficznego.
Hierarchia kluczy
Klucze można wyprowadzić z innych kluczy za pomocą funkcji derywacji klucza (KDF), takiej jak HKDF, co powoduje powstanie hierarchii kluczy.
Na poniższym diagramie przedstawiono typową hierarchię kluczy w przypadku FBE, gdy nie używa się kluczy spakowanych w sprzęcie:
Klucz klasy FBE to surowy klucz szyfrowania, który Android przekazuje jądrowi systemu Linux, aby odblokować określony zestaw zaszyfrowanych katalogów, np. pamięć szyfrowana hasłem dla konkretnego użytkownika Androida. W jądrze ten klucz jest nazywany kluczem głównym fscrypt. Z tego klucza jądro uzyskuje te podklucze:
- Identyfikator klucza. Nie jest ona używana do szyfrowania, lecz używana do identyfikowania klucza, za pomocą którego chroniony jest określony plik lub katalog.
- Klucz szyfrowania zawartości pliku
- Klucz szyfrowania nazw plików
Natomiast na poniższym diagramie przedstawiono hierarchię kluczy w przypadku szyfrowania po stronie klienta z użyciem kluczy szyfrujących sprzętowych:
W porównaniu z poprzednim przykładem do hierarchii kluczy dodano dodatkowy poziom, a klucz szyfrowania zawartości pliku został przeniesiony. Węzeł główny nadal reprezentuje klucz, który Android przekazuje do Linuksa, aby odblokować zestaw zaszyfrowanych katalogów. Klucz jest jednak w tym przypadku w kształcie tymczasowym i aby go użyć, musi zostać przekazany do dedykowanego sprzętu. Sprzęt musi mieć 2 interfejsy, które przyjmują klucz zapakowany na bieżąco:
- Jeden interfejs do wyprowadzania
inline_encryption_key
i bezpośredniego programowania w kluczu slotu wbudowanego silnika szyfrowania. Umożliwia to szyfrowanie/odszyfrowywanie zawartości plików bez dostępu do nieprzetworzonego klucza przez oprogramowanie. W popularnych jądrach Androida ten interfejs odpowiada operacjiblk_crypto_ll_ops::keyslot_program
, która musi zostać zaimplementowana przez sterownik pamięci. - Jeden interfejs do wyprowadzenia i zwrotu
sw_secret
(„tajemnica oprogramowania” – w niektórych miejscach nazywana też „tajemnicą surową”), który jest kluczem używanym przez Linuksa do wyprowadzenia kluczy podrzędnych do wszystkiego oprócz szyfrowania zawartości pliku. W popularnych jądrach Androida ten interfejs odpowiada operacjiblk_crypto_ll_ops::derive_sw_secret
, która musi zostać zaimplementowana przez sterownik pamięci.
Aby wyprowadzić inline_encryption_key
i sw_secret
z klucza magazynowania w postaci surowych danych, sprzęt musi używać silnego generatora kluczy KDF. Ten algorytm KDF musi być zgodny ze sprawdzonymi metodami kryptografii. Jego moc bezpieczeństwa musi wynosić co najmniej 256 bitów, co wystarcza dla każdego algorytmu używanego później. Podczas wyprowadzania każdego typu klucza podrzędnego należy też używać osobnej etykiety, kontekstu i ciągu informacji specyficznych dla aplikacji, aby zagwarantować, że otrzymane klucze podrzędne są od siebie odseparowane pod względem kryptograficznym, czyli że znajomość jednego z nich nie ujawnia informacji o żadnym innym. Rozciąganie klucza nie jest wymagane, ponieważ surowy klucz pamięci jest już kluczem losowym o jednolitej rozkładzie.
Technicznie rzecz biorąc, można użyć dowolnego KDF, który spełnia wymagania dotyczące bezpieczeństwa.
Jednak na potrzeby testów należy ponownie zaimplementować ten sam plik KDF w kodzie testowym. Obecnie sprawdzona i wdrożona 1 funkcja KDF znajduje się w kodzie źródłowym vts_kernel_encryption_test
.
Zalecamy, aby sprzęt używał tego KDF, który korzysta z NIST SP 800-108 „KDF w trybie przeciwdziałania” z AES-256-CMAC jako PRF. Pamiętaj, że aby zapewnić zgodność, wszystkie części algorytmu muszą być identyczne, w tym wybór kontekstów KDF i etykietek dla każdego klucza podrzędnego.
Zawijanie kluczy
Aby spełnić wymagania dotyczące zabezpieczeń kluczy spakowanych w sprzęt, zdefiniowano 2 typy pakowania kluczy:
- Efemerydyczny owijacz: sprzęt szyfruje klucz pierwotny za pomocą klucza, który jest generowany losowo przy każdym uruchomieniu i nie jest bezpośrednio dostępny poza sprzętem.
- Pakowanie długoterminowe: sprzęt szyfruje klucz w postaci danych nieprzetworzonych za pomocą unikalnego, trwałego klucza wbudowanego w sprzęcie, który nie jest bezpośrednio dostępny poza sprzętem.
Wszystkie klucze przekazywane do jądra Linuksa w celu odblokowania pamięci są tymczasowo spakowane. Dzięki temu, jeśli atakujący wydobędzie klucz używany z pamięci systemowej, nie będzie mógł go użyć nie tylko na wyłączonym urządzeniu, ale także po jego ponownym uruchomieniu.
Jednocześnie Android musi mieć możliwość przechowywania zaszyfrowanej wersji kluczy na dysku, aby można było je odblokować. Do tego celu nadają się klucze w postaci tekstu zwykłego. Zaleca się jednak, aby klucze w postaci zwykłego tekstu nigdy nie były obecne w pamięci systemowej, aby nie można było ich wyodrębnić i użyć poza urządzeniem, nawet jeśli wyodrębnienie nastąpiło w momencie uruchamiania. Z tego powodu zdefiniowano pojęcie długoterminowego opakowania.
Aby obsługiwać zarządzanie kluczami w tych 2 sposobach, sprzęt musi implementować te interfejsy:
- Interfejsy do generowania i importowania kluczy pamięci masowej, które zwracają je w długoterminowej zapakowanej postaci. Dostęp do tych interfejsów jest pośrednio uzyskiwany przez KeyMint i odpowiada tagowi
TAG_STORAGE_KEY
KeyMint. Funkcja „generuj” jest używana przezvold
do generowania nowych kluczy pamięci masowej na potrzeby Androida, a funkcja „importuj” jest używana przezvts_kernel_encryption_test
do importowania kluczy testowych. - Interfejs umożliwiający konwersję długoterminowego opakowanego klucza pamięci masowej na opakowany efemerycznie klucz pamięci masowej. Odpowiada to metodzie
convertStorageKeyToEphemeral
KeyMint. Ta metoda jest używana przezvold
ivts_kernel_encryption_test
do odblokowania pamięci.
Algorytm szyfrowania klucza to szczegół implementacji, ale powinien używać silnego algorytmu AEAD, takiego jak AES-256-GCM z losowymi wartościami IV.
Wymagane zmiany w oprogramowaniu
AOSP zawiera już podstawowy framework do obsługi kluczy zaszyfrowanych sprzętowo. Obejmuje to obsługę w komponentach przestrzeni użytkownika, takich jak vold
, a także obsługę jądra Linuxa w blk-crypto, fscrypt i dm-default-key.
Wymagane są jednak pewne zmiany związane z wdrożeniem.
Zmiany w KeyMint
Implementacja KeyMint na urządzeniu musi zostać zmodyfikowana, aby obsługiwać metodę TAG_STORAGE_KEY
i implementować metodę convertStorageKeyToEphemeral
.
W Keymaster zamiast convertStorageKeyToEphemeral
użyto exportKey
.
Zmiany w jądrze Linuksa
Sterownik jądra Linuxa dla wbudowanego modułu szyfrowania urządzenia musi zostać zmodyfikowany, aby obsługiwał klucze szyfrowane sprzętowo.
W przypadku jąder android14
i wyższych:
ustaw BLK_CRYPTO_KEY_TYPE_HW_WRAPPED
w blk_crypto_profile::key_types_supported
,
wykonaj blk_crypto_ll_ops::keyslot_program
i blk_crypto_ll_ops::keyslot_evict
obsługuj programowanie i usuwanie kluczy w ramach szyfrowania sprzętowego
i wdróż blk_crypto_ll_ops::derive_sw_secret
.
W przypadku jąder android12
i android13
:
ustaw BLK_CRYPTO_FEATURE_WRAPPED_KEYS
w blk_keyslot_manager::features
,
upewnij się, że blk_ksm_ll_ops::keyslot_program
i blk_ksm_ll_ops::keyslot_evict
obsługują programowanie i wyrzucanie kluczy sprzętowych oraz zaimplementuj blk_ksm_ll_ops::derive_raw_secret
.
W przypadku jąder android11
ustaw BLK_CRYPTO_FEATURE_WRAPPED_KEYS
w keyslot_manager::features
, wykonaj keyslot_mgmt_ll_ops::keyslot_program
i keyslot_mgmt_ll_ops::keyslot_evict
, obsłuż programowanie i wyrzucanie kluczy w ramach sprzętowych oraz wdrożenie keyslot_mgmt_ll_ops::derive_raw_secret
.
Testowanie
Chociaż szyfrowanie za pomocą kluczy sprzętowych jest trudniejsze do przetestowania niż szyfrowanie za pomocą standardowych kluczy, można je przetestować, importując klucz testowy i ponownie implementując wyprowadzenie klucza, które wykonuje sprzęt. Jest to implementowane w vts_kernel_encryption_test
. Aby przeprowadzić ten test:
atest -v vts_kernel_encryption_test
Przeczytaj dziennik testu i sprawdź, czy przypadki testowe kluczy zaszyfrowanych sprzętowo (na przykład FBEPolicyTest.TestAesInlineCryptOptimizedHwWrappedKeyPolicy
i DmDefaultKeyTest.TestHwWrappedKey
) nie zostały pominięte z powodu braku obsługi kluczy zaszyfrowanych sprzętowo, ponieważ w takim przypadku wyniki testu są nadal „pozytywne”.
Włączanie klawiszy
Gdy obsługa kluczy sprzętowych zapakowanych w sprzęcie na urządzeniu będzie działać prawidłowo, możesz wprowadzić w pliku fstab
urządzenia te zmiany, aby Android używał go do szyfrowania metadanych i szyfrowania pełnego dysku:
- FBE: dodaj flagę
wrappedkey_v0
do parametrufileencryption
. Użyj na przykład elementufileencryption=::inlinecrypt_optimized+wrappedkey_v0
. Więcej informacji znajdziesz w dokumentacji FBE. - Szyfrowanie metadanych: dodaj flagę
wrappedkey_v0
do parametrumetadata_encryption
. Użyj na przykład elementumetadata_encryption=:wrappedkey_v0
. Więcej informacji znajdziesz w dokumentacji dotyczącej szyfrowania metadanych.