Zapis i odczyt szyfrowanych plików w systemie Android

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