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