Zabezpieczenie danych w SharedPreferences przy pomocy EncryptedSharedPreferences

Nie ma nikogo z piszących aplikacje na Androida, kto nie korzystałby z najbardziej podstawowej metody na przechowywanie danych, jaką jest interfejs SharedPreferences, ponieważ jest on najszybszym sposobem na stałe przechowanie wartości, które nie są usuwane razem z zamknięciem aplikacji. Czy w każdym przypadku jest to dobre rozwiązanie?

Sama zasada działania SharedPreferences jest prosta. Jej zadaniem jest przechowanie na stałe wartości pod postacią klucz - wartość w pliku XML w pamięci wewnętrznej naszego telefonu. Z zasady nie powinno się wykorzystywać tego interfejsu do przechowywania dużych danych, a jedynie przykładowo ustawień aplikacji, np.: jednostka temperatury w aplikacji pogodowej. Bywa jednak, że preferencje są wykorzystywane do zapisywania danych, które mogą mieć wpływ na bezpieczeństwo danych używanych w aplikacji. Spotkałem się z zapisywaniem tam loginów i haseł użytkowników w celu automatycznego ich ponownego zalogowania. Jednak czy takie wykorzystanie SharedPreferences jest właściwe? Zobaczmy przykładową zawartość pliku XML z tego typu danymi:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>  
<map>  
    <string name="password">bardzo_tajne_haslo</string>  
    <boolean name="remember_user" value="true" />  
    <string name="user_name">login</string>  
</map>  

Tak prezentuje się kod, który zapisuje te wartości:

val sharedPreferences = getPreferences(Context.MODE_PRIVATE)  

sharedPreferences.edit()  
    .putString("user_name", "login")  
    .putString("password", "bardzo_tajne_haslo")  
    .putBoolean("remember_user", true)  
    .apply()  

Widać tutaj, że możemy z wyjątkową łatwością odczytać nazwę oraz hasło użytkownika naszej aplikacji. Czy jest to bezpieczne? Nie sądzę. Jednak jest na to bardzo proste rozwiązanie.

EncryptedSharedPreferences

Android Jetpack wprowadził bibliotekę Security, dzięki której jesteśmy w stanie zabezpieczyć SharedPreferences przy pomocy interfejsu EncryptedSharedPreferences. Jednym z minusów tego rozwiązania jest to, że minimalna wersja SDK w naszej aplikacji musi zostać ustawiona na wersję 21 (Android 5.0 Lillipop). Bibliotekę należy dodać do pliku build.gradle naszego modułu aplikacji:

android {  
    defaultConfig {  
        minSdkVersion 21  
    }  
}  

dependencies {  
    implementation 'androidx.security:security-crypto:1.1.0-alpha01'  
}  

EncryptedSharedPreferences wykorzystuje interfejs AndroidKeyStore (jednak nie dla API 21 oraz 22), opakowuje standardowy interfejs SharedPreferences oraz automatycznie szyfruje klucze i wartości:

  • klucze są szyfrowane przy pomocy deterministycznego algorytmu szyfrowania, dzięki czemu klucz może być zaszyfrowany i odpowiednio wyszukany,
  • wartości są szyfrowane przy pomocy algorytmu AES-256 GCM i są niedeterministyczne.

Zobaczmy jak bezpiecznie zapisać wartości wcześniej podane jako przykład, przy pomocy interfejsu EncryptedSharedPreferences:

val masterKeyAlias = MasterKey.Builder(applicationContext)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build()

val sharedPreferences = EncryptedSharedPreferences
    .create(
        applicationContext,  
        "nazwa_pliku_preferencji",
        masterKeyAlias,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )

sharedPreferences.edit()  
    .putString("user_name", "login")  
    .putString("password", "bardzo_tajne_haslo")  
    .putBoolean("remember_user", true)  
    .apply()  

Zobaczmy teraz rezultat takiej operacji:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>  
<map>  
    <string name="ARZh9qoeP3w0SJdYFNaUJNzYU2yqR2nIo96yUXSD">AT/N5tPK4aJIL1IHiEPYXco+wHqXl0hH2xQwyiFklWo5L8wnB0ncQ9fAaJOiMbkTmw==</string>  
    <string name="__androidx_security_crypto_encrypted_prefs_key_keyset__">12a90141f97f8d4c97e5d72e453250ac14bd1fc9f9d8595241f1b48846ee38fc9f7315a290ff9c30fa6ac34a5c656cd3949b024a2fbf1a65b32118844f507b3c76938c79c302d10fadd6f184e3ff3ac9680938083d026e8bbe872b488620a2a7ec5b44d2cee3a4cfb7fc0f973b6e5a05147d87f45ef36bc726ec653c5fb678545e5aa36f05bb02b0e63c8ef4c05dd2103c243af6e85c591a72364e36d6d88972f6fd8a4aebf50b4c59cc6e441a4408aaed87b301123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e4165735369764b6579100118aaed87b3012001</string>  
    <string name="__androidx_security_crypto_encrypted_prefs_value_keyset__">128801adf19fe96e7ce7ff5ca9740876eb12c1e1b6cb36bd01f676b03e64f7334b15bbe09d0378c0635baa5a452248726a81957a415321776c039656b583ce6c243abb5d29d99d31b2e496bec34c95ec500d8f7befd7b247a8a5d579686e396fcfc9cff3689c485fd0fb55e52c819fa94bff94d4a79361442fa406205bb8d4806b3ca091857377ede0d51f1a4408d3cdb7fe03123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e41657347636d4b6579100118d3cdb7fe032001</string>  
    <string name="ARZh9qrtq9Q086RbyrlVCqda+FdP81GnfifPpCI=">AT/N5tNGC0HBDxbdKnh/9J5gi0nhQDMKvsG6fwQ9n+EHs/9hoB/2buHGSGGwKEbVDTxepvOYUxBQWvU=</string>  
    <string name="ARZh9qr9OVA4aYZZx+BbqhjhwMuEySl/H+yYjwzAYzFUWg==">AT/N5tOeRvOVo357KJ3FDfKf8NfS5pTaQWJgU3SZ0O/U5Eymmzk=</string>  
</map>  

Trzeba przyznać, że powyższy efekt robi wrażenie i w tym momencie klucze i wartości są zabezpieczone w taki sposób, że nie można odkryć co sobą prezentują i jakie mają wartości.

Wpływ na wydajność

Zastosowanie szyfrowania niestety ma swój wpływ na wydajność i EncryptedSharedPreferences w porównaniu do zwykłego interfejsu SharedPreferences jest wolniejsze. Zobaczmy jak wygląda porównanie operacji inicjalizacji / otwierania, zapisu i odczytu wartości w obu przykładowych przypadkach (aplikacja w wersji release na fizycznym urządzeniu Pixel 2, na innych urządzeniach czasy mogą być inne):

Pierwsze uruchomienie aplikacji:

  EncryptedSharedPreferences SharedPreferences
Inicjalizacja / otwarcie 292 ms 2 ms
Zapis 4 ms 3 ms
Odczyt 1 ms 0 ms

Drugie uruchomienie aplikacji:

  EncryptedSharedPreferences SharedPreferences
Inicjalizacja / otwarcie 231 ms 1 ms
Zapis 6 ms 0 ms
Odczyt 2 ms 0 ms

Widzimy tutaj, że różnica w operacjach zapisu i odczytu nie jest szczególnie znaczna i nie powinna mieć większego znaczenia, a w przypadku EncryptedSharedPreferences najbardziej czasochłonnym jest zadanie inicjalizacji / otwarcia więc trzeba mieć na uwadze, żeby akurat ten element nie zaburzył pracy aplikacji.

Podsumowanie

Interfejs EncryptedSharedPreferences jest bardzo dobrym rozwiązaniam w przypadku, kiedy nasza aplikacja musi przechować na stałe jakieś wrażliwe informacje. Jest to bardzo wygodny i bezpieczny sposób na przechowywanie takich danych, które w standardowym SharedPreferences są łatwe do odczytu.

W związku z tym, że użycie bezpiecznego interfejsu jest jednak mniej wydajne, zalecane jest, aby zapisywać tam tylko te dane, które faktycznie tego wymagają, nie należy używać go jako zamiennika standardowych preferencji do każdego zastosowania. Pamiętajmy również o tym, aby zapisywać tam małe ilości informacji (nie tylko w EncryptedSharedPreferences, ale również w SharedPreferences).

Zapis i odczyt szyfrowanych plików w systemie Android

Przy pomocy tej biblioteki można szyfrować nie tylko SharedPreferences, ale również dane zapisywane w plikach. Wejdź na poniższą stronę i dowiedz się w jaki sposób tego dokonać:

Zapis i odczyt szyfrowanych plików w systemie Android