Podobnie jak większość oprogramowania do szyfrowania dysków i plików, szyfrowanie pamięci w Androidzie tradycyjnie polega na tym, że w pamięci systemowej znajdują się nieprzetworzone klucze szyfrowania, dzięki którym można przeprowadzić szyfrowanie. Nawet jeśli szyfrowanie jest wykonywane przez dedykowany sprzęt, a nie oprogramowanie, to oprogramowanie nadal musi zarządzać nieprzetworzonymi kluczami szyfrowania.
Zwykle nie jest to uważane za problem, ponieważ podczas ataku offline klucze nie są obecne, a to jest główny typ ataku, przed którym ma chronić 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, opakowywanie kluczy magazynu w formach krótko- i długoterminowych, 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 (lub wbudowany sprzęt do szyfrowania) to sprzęt, który szyfruje/odszyfrowuje dane podczas ich przesyłania do lub z urządzenia magazynującego. Zwykle jest to host UFS lub kontroler eMMC, który implementuje rozszerzenia szyfrowania zdefiniowane w odpowiedniej specyfikacji JEDEC.
Projektowanie
Ta sekcja przedstawia projekt funkcji kluczy w ramach oprogramowania, w tym wymagania dotyczące sprzętu. Ta dyskusja koncentruje się na szyfrowaniu plików, ale rozwiązanie dotyczy również 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. Takie podejście ma jednak pewne wady:
- Liczba kluczy szyfrowania może przekroczyć liczbę sekcji kluczy.
- Wbudowane mechanizmy szyfrowania zwykle tracą zawartość swoich kluczy, jeśli kontroler pamięci (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żywane jest szyfrowanie w ramach, system operacyjny musi być zawsze gotowy do przeprogramowania slotów kluczy bez udziału użytkownika.
- Wbudowane mechanizmy szyfrowania można używać tylko do szyfrowania i 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. Dzięki temu można obsługiwać nieograniczoną liczbę kluczy. Dodatkowo hierarchia kluczy jest modyfikowana i częściowo przenoszona na ten sprzęt, co pozwala na zwrócenie klucza podrzędnego do oprogramowania w przypadku zadań, które nie mogą korzystać z wbudowanego mechanizmu szyfrowania.
Hierarchia kluczy
Klucze można wyprowadzić z innych kluczy za pomocą funkcji derywacji klucza (KDF), takiej jak HKDF, co tworzy hierarchię kluczy.
Poniższy diagram przedstawia 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, na przykład pamięć szyfrowana hasłem dla konkretnego użytkownika Androida. (W jądrze ten klucz jest nazywany kluczem głównym fscrypt). Na podstawie tego klucza jądro wylicza te klucze podrzędne:
- Identyfikator klucza. Nie jest on używany do szyfrowania, ale jest to wartość służąca do identyfikacji klucza, którym jest chroniony dany 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 w hierarchii kluczy dodano dodatkowy poziom, a klucz szyfrowania zawartości pliku został przeniesiony. Węzeł główny nadal reprezentuje klucz, który Android przekazuje Linuksowi w celu odblokowania zestawu 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 implementować 2 interfejsy, które przyjmują klucz zapakowany w chwilową wartość:
- Jeden interfejs do wyprowadzania
inline_encryption_key
i bezpośredniego programowania w kluczu slotu wbudowanego silnika szyfrowania. Umożliwia to szyfrowanie/odszyfrowywanie treści pliku bez konieczności uzyskiwania przez oprogramowanie dostępu do klucza w postaci surowych danych. W jądrach wspólnych Androida ten interfejs odpowiada operacjiblk_crypto_ll_ops::keyslot_program
, która musi być zaimplementowana przez sterownik pamięci masowej. - Jeden interfejs do wyprowadzenia i zwrotu
sw_secret
(„tajemnicy oprogramowania” – w niektórych miejscach nazywanej też „tajemnicą surową”), która jest kluczem używanym przez Linuksa do wyprowadzenia kluczy podrzędnych do wszystkiego oprócz szyfrowania zawartości pliku. W jądrach wspólnych Androida ten interfejs odpowiada operacjiblk_crypto_ll_ops::derive_sw_secret
, która musi być zaimplementowana przez sterownik pamięci masowej.
Aby wyprowadzić inline_encryption_key
i sw_secret
z klucza magazynowania w postaci surowych danych, sprzęt musi używać silnego klucza 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 specyficznego 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.
Teoretycznie można użyć dowolnego klucza 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 sprawdzono i wdrożono 1 KDF. Znajdziesz go 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 etykiet dla każdego klucza podrzędnego.
Opakowanie klucza
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 wsadowy za pomocą unikalnego, trwałego klucza wbudowanego w sprzęt, 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 opakowane. 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 danych nieprzetworzonych. Zaleca się jednak, aby klucze w postaci zwykłych danych nigdy nie były obecne w pamięci systemowej, aby nie można było ich wyodrębnić i używać 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 są zwracane w długoterminowej formie zapakowanej; Do tych interfejsów uzyskuje się dostęp pośrednio przez KeyMint. Odpowiadają one 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 do konwertowania długoterminowego klucza zasobów w pakiecie na klucz zasobów w pakiecie krótkotrwałym. Odpowiada to metodzie
convertStorageKeyToEphemeral
KeyMint. Ta metoda jest używana zarówno przezvold
, jak 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 implementacją.
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 Linuksa 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 log testu i sprawdź, czy przypadki testowe kluczy z opakowaniem sprzętowym (np. FBEPolicyTest.TestAesInlineCryptOptimizedHwWrappedKeyPolicy
i DmDefaultKeyTest.TestHwWrappedKey
) nie zostały pominięte z powodu braku obsługi kluczy z opakowaniem sprzętowym, ponieważ w tym przypadku wyniki testu nadal będą „przeszłonięte”.
Włączanie klawiszy
Gdy obsługa kluczy sprzętowych zapakowanych w urządzenie będzie działać prawidłowo, możesz wprowadzić w pliku fstab
urządzenia te zmiany, aby Android używał go do szyfrowania FBE i metadanych:
- FBE: dodaj do parametru
fileencryption
flagęwrappedkey_v0
. 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.