Bardzo często wykorzystywanym sposobem na stały zapis informacji w systemie Android jest zapis danych w pamięci urządzenia pod postacią plików. Niestety, nawet pomimo tego, że pliki zapiszemy w pamięci wewnętrznej urządzenia zamiast, przykładowo, na karcie pamięci, to istnieją sytuacje, w których wrażliwe informacje w nich zawarte mogą zostać z łatwością odczytane (np. urządzenie z uprawnieniami root'a).
Standardowy zapis i odczyt plików
Operacja zapisu i odczytu pliku na Androidzie jest bardzo prosta. Najpierw tworzymy obiekt klasy File
, który wskazuje lokalizację pliku, a operacje zapisu oraz odczytu wykonywane są kolejno przy pomocy FileOutputStream
oraz FileInputStream
:
// Dane do zapisu w pliku
val fileContent = "Some data"
// Wskazujemy lokalizację zapisu i nazwę pliku
val file = File(applicationContext.filesDir, "file_name")
// Zapis do pliku
val outputStream: FileOutputStream = file.outputStream()
outputStream.use { fileOutputStream ->
fileOutputStream.write(fileContent.toByteArray())
fileOutputStream.close()
}
// Odczyt z pliku
val inputStream: FileInputStream = file.inputStream()
inputStream.use { fileInputStream ->
val inputStreamReader = InputStreamReader(fileInputStream)
val bufferedReader = BufferedReader(inputStreamReader)
var receiveString: String? = ""
val stringBuilder = StringBuilder()
while (bufferedReader.readLine().also { receiveString = it } != null) {
stringBuilder.append(receiveString)
}
bufferedReader.close()
inputStreamReader.close()
fileInputStream.close()
// Odczytana zawartość pliku
val readedContent = stringBuilder.toString()
}
EncryptedFile - sposób na szyfrowanie plików
Aby móc skorzystać z szyfrowania zaoferowanego przez EncryptedFile
z biblioteki Security Android Jetpack musimy do pliku build.gradle
modułu aplikacji dodać poniższą zależność. Należy przy okazji pamiętać, że minimalna wersja SDK musi zostać ustawiona na wersję 21 (Android 5.0 Lillipop).
android {
defaultConfig {
minSdkVersion 21
}
}
dependencies {
implementation 'androidx.security:security-crypto:1.1.0-alpha01'
}
Aby dodatkowo plik został zaszyfrowany, nie trzeba wykonywać wielu skomplikowanych kroków. Powyższy kod należy jedynie wzbogacić o kilka dodatków.
Pierwszym z nich jest pobranie MasterKey
, a następnie przekazanie go razem z obiektem klasy File
do buildera klasy EncryptedFile
, która to właśnie jest odpowiedzialna za zaszyfrowanie oraz odszyfrowanie jego zawartości.
W tym przypadku również korzystamy standardowo z FileOutputStream
do zapisu pliku oraz z FileInputStream
do jego odczytu. W tym jednak przypadku nie są one tworzone bezpośrednio z obiektu klasy File
, a z nowej klasy EncryptedFile
przy pomocy metod kolejno openFileOutput
oraz openFileInput
.
Zobaczmy jak wygląda w całości zmodyfikowany kod służący do zapisu oraz odczytu zaszyfrowanych plików:
// Dane do zapisu w pliku
val fileContent = "Some data"
// Wskazujemy lokalizację zapisu i nazwę pliku
val file = File(applicationContext.filesDir, "file_name")
// Tworzymy klucz Android Keystore
val masterKeyAlias: MasterKey = MasterKey.Builder(applicationContext)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
// Tworzymy obiekt klasy EncryptedFile
val encryptedFile: EncryptedFile = EncryptedFile.Builder(
applicationContext,
file,
masterKeyAlias,
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
// Zapis do pliku
val outputStream: FileOutputStream = encryptedFile.openFileOutput()
outputStream.use { fileOutputStream ->
fileOutputStream.write(fileContent.toByteArray())
fileOutputStream.close()
}
// Odczyt z pliku
val inputStream: FileInputStream = encryptedFile.openFileInput()
inputStream.use { fileInputStream ->
val inputStreamReader = InputStreamReader(fileInputStream)
val bufferedReader = BufferedReader(inputStreamReader)
var receiveString: String? = ""
val stringBuilder = StringBuilder()
while (bufferedReader.readLine().also { receiveString = it } != null) {
stringBuilder.append(receiveString)
}
bufferedReader.close()
inputStreamReader.close()
fileInputStream.close()
// Odczytana zawartość pliku
val readedContent = stringBuilder.toString()
}
Wpływ szyfrowania na czas odczytu i zapisu pliku
Wykonywanie dodatkowych operacji takich jak szyfrowanie i odszyfrowywanie plików jest niestety bardziej czasochłonnym zadaniem w porównaniu do zapisu i odczytu niezaszyfrowanych plików. Poniżej można zobaczyć jak poszczególne operacje wykonują się w aplikacji w trybie produkcyjnym na fizycznym urządzeniu Pixel 2 (są to oczywiście przykładowe wyniki i będą się one różniły również na innych urządzeniach).
Plik tekstowy w formacie JSON o rozmiarze 100kB:
EncryptedFile | File | |
---|---|---|
Inicjalizacja / otwarcie | 459 ms | 0 ms |
Zapis | 20 ms | 6 ms |
Odczyt | 21 ms | 9 ms |
Plik tekstowy w formacie JSON o rozmiarze 10MB:
EncryptedFile | File | |
---|---|---|
Inicjalizacja / otwarcie | 183 ms | 0 ms |
Zapis | 1326 ms | 477 ms |
Odczyt | 1430 ms | 930 ms |
Wyraźnie widać tutaj zdecydowaną zależność czasu wykonywania odczytu i zapisu pliku od jego rozmiaru. Warto mieć to na uwadze i szyfrować faktycznie tylko te informacje, które zagrażają bezpieczeństwu danych, które nasza aplikacja zapisuje pod postacią plików.
Podsumowanie
Tworząc aplikację mobilną musimy mieć na uwadze to, że naszym obowiązkiem jest zapewnienie jak największego bezpieczeństwa danych, które aplikacja przetwarza.
Powyższy przykład jest tylko jednym ze sposobów na zabezpieczenie zapisywanych plików. Istnieje zdecydowanie więcej bibliotek, które bardzo nam w tym pomogą.
Zabezpieczenie danych w SharedPreferences przy pomocy EncryptedSharedPreferences
Przy pomocy tej biblioteki można szyfrować nie tylko pliki, ale również dane zapisywane w SharedPreferences. Wejdź na poniższą stronę i dowiedz się w jaki sposób tego dokonać:
Zabezpieczenie danych w SharedPreferences przy pomocy EncryptedSharedPreferences