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: inline cryptographic engine (lub inline hardware encryption) to sprzęt, który szyfruje/odszyfrowuje dane podczas ich przesyłania do lub 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
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 silnika szyfrowania. Takie podejście ma jednak pewne wady:
- Liczba kluczy szyfrowania może przekroczyć liczbę sekcji kluczy.
- Wbudowane mechanizmy szyfrowania zazwyczaj tracą zawartość swoich kluczy, jeśli kontroler pamięci masowej (zazwyczaj 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 wbudowane, 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 nadal 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 szyfrujący, który Android przekazuje jądrowi systemu Linux, aby odblokować określony zestaw zaszyfrowanych katalogów, na przykład pamięć szyfrowana za pomocą poświadczeń dla konkretnego użytkownika Androida. (W jądrze ten klucz jest nazywany kluczem głównym fscrypt). Z tego klucza jądro wywodzi 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 zaszyfrowany w tymczasowej formie i aby go użyć, musi zostać przekazany do dedykowanego sprzętu. Sprzęt musi implementować 2 interfejsy, które przyjmują klucz zapakowany w chwilowy obiekt:
- 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 dostępu oprogramowania do klucza w postaci binarnej. W ogólnych jądrach 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 ogólnych jądrach 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 surowego klucza magazynowania, sprzęt musi używać silnego kryptograficznie 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.
Teoretycznie można użyć dowolnego klucza KDF, który spełnia wymagania dotyczące bezpieczeństwa.
Jednak na potrzeby testowania 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 etykietek 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 surowy za pomocą klucza, który jest generowany losowo przy każdym uruchomieniu i nie jest bezpośrednio dostępny poza sprzętem.
- Długoterminowe opakowanie: sprzęt szyfruje klucz pierwotny 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 tekstu zwykłego. 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 trwałej formie; Dostęp do tych interfejsów jest możliwy pośrednio przez KeyMint i 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 spakowanego w pamięć na klucz spakowany w pamięć krótkotrwałą. Odpowiada to metodzie
convertStorageKeyToEphemeral
KeyMint. Ta metoda jest używana przezvold
ivts_kernel_encryption_test
w celu odblokowania miejsca na dane.
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.
Konieczne 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 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/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 dotyczące kluczy zaszyfrowanych sprzętowo (np. 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 nadal będą „przeszłone”.
Włączanie klawiszy
Gdy obsługa kluczy sprzętowych zapakowanych w 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 FBE i metadanych:
- FBE: dodaj do parametru
fileencryption
flagęwrappedkey_v0
. Użyj na przykład funkcjifileencryption=::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.