Блог Roxie Mobile

Обеспечение безопасности в Androind приложениях

24 марта 2020 3 793
Android разработка, Безопасность

Безопасность, несомненно, важная часть любого мобильного приложения. К сожалению, обеспечить реальную безопасность – непростая задача. Недавно Google выпустил библиотеку «security-crypto» в составе компонентов Jetpack, чтобы облегчить разработку безопасных приложений:
В настоящий момент библиотека состоит из трёх основных частей:

  • MasterKeys
  • EncryptedSharedPreferences
  • EncryptedFile

Внутри AndroidX Security использует библиотеку Tink. Tink — это библиотека с открытым исходным кодом от Google, предоставляющая криптографические API.

Установка

Чтобы воспользоваться библиотекой, нужно добавить всего одну строчку в Gradle-файл вашего приложения:

1
implementation `androidx.security:security-crypto:1.0.0-beta01`

В настоящее время «security-crypto» находится в beta-режиме, однако на странице mvnrepository можно следить за выходом новых версий. Она действительно очень маленькая — после добавления библиотеки размер файла .apk увеличился всего на 11,9 Кб.
Чтобы использовать библиотеку, нужно задать minSdkVersion 23+, потому что в новом API операции KeyStore сделали более стабильными (в API 23 KeyPairGeneratorSpec был заменён на KeyGenParameterSpec).

MasterKeys

MasterKeys — вспомогательный класс всего с одним публичным методом getOrCreate(…). Он позволяет создать мастер-ключ, а затем получить для него псевдоним. Проанализируем код:

1
2
3
4
5
6
7
8
9
10
@NonNull
public static String getOrCreate(
        @NonNull KeyGenParameterSpec keyGenParameterSpec)
        throws GeneralSecurityException, IOException {
    validate(keyGenParameterSpec);
    if (!MasterKeys.keyExists(keyGenParameterSpec.getKeystoreAlias())) {
        generateKey(keyGenParameterSpec);
    }
    return keyGenParameterSpec.getKeystoreAlias();
}

Единственный параметр, KeyGenParameterSpec, позволяет задавать такие настройки, как алгоритм, режим шифрования, выравнивание и размер ключа. Значение по умолчанию, присвоенное MasterKeys.AES256_GCM_SPEC, создает следующий объект:

1
2
3
4
5
6
7
8
9
10
11
@NonNull
private static KeyGenParameterSpec createAES256GCMKeyGenParameterSpec(
        @NonNull String keyAlias) {
    KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
            keyAlias,
            KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setKeySize(KEY_SIZE);
    return builder.build();
}

Где keyAlias принимает значение _androidx_security_master_key_, а KEY_SIZE = 256.

EncryptedSharedPreferences

EncryptedSharedPreferences — класс-обёртка для SharedPreferences. Он позволяет сохранять и читать значения, при этом шифрование и расшифровывание данных происходят уже внутри него. Посмотрим, как создаётся его экземпляр:

1
2
3
4
5
6
7
EncryptedSharedPreferences.create(
    PREFS_FILENAME,
    masterKeyAlias,
    applicationContext,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

Второй параметр — это псевдоним, сгенерированный методом MasterKeys.getOrCreate(KeyGenParameterSpec). Последние два параметра задают схемы, используемые для шифрования ключей и значений. Сейчас единственное возможное значение для EncryptedSharedPreferences.PrefKeyEncryptionScheme — это AES256_SIV, а для EncryptedSharedPreferences.PrefValueEncryptionScheme — это AES256_GCM.
После этого мы можем использовать его как обычный объект SharedPreferences. Например, так выглядит сохранение String:

1
2
3
4
encryptedPrefs.edit {
    putString(ENC_KEY, value)
    apply()
}

А так чтение сохранённого значения:

1
val string = encryptedPrefs.getString(ENC_KEY, null)

EncryptedFile

EncryptedFile позволяет легко зашифровывать данные с помощью FileInputStream и расшифровывать их с помощью FileOutputStream. Чтобы создать экземпляр EncryptedFile, нам нужно сделать следующее:

1
2
3
4
5
6
EncryptedFile.Builder(
    file,
    applicationContext,
    masterKeyAlias,
    EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()

Первый параметр — это объект File, задающий путь и название файла с зашифрованными данными. В моём случае код выглядит следующим образом:

1
val file = File(filesDir, ENCRYPTED_FILE_NAME)

Значение masterKeyAlias задаётся точно так же, как для EncryptedSharedPreferences. Последний параметр, fileEncryptionScheme, задаёт схему шифрования потока ввода/вывода. В данный момент единственное возможное значение — это AES256_GCM_HKDF_4KB. Привожу характеристики данной схемы:

  • Размер основного ключа: 32 байта
  • Алгоритм HKDF: HMAC-SHA256
  • Размер производных ключей AES-GCM: 32 байта
  • Размер сегмента зашифрованного текста: 4096 байт

Для иллюстрации Я решил загрузить файл README.md с помощью библиотеки OkHttp из репозитория с примерами (ссылка здесь), считать байты ответа из response.body!!.Bytes(), а затем сохранить файл, передав байты в следующий метод:

1
2
3
4
5
6
7
8
9
10
11
12
private fun onFileDownloaded(bytes: ByteArray) {
    var encryptedOutputStream: FileOutputStream? = null
    try {
        encryptedOutputStream = encryptedFile.openFileOutput().apply {
            write(bytes)
        }
    } catch (e: Exception) {
        Log.e(TAG, "Could not open encrypted file", e)
    } finally {
        encryptedOutputStream?.close()
    }
}

А затем прочитать их:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private fun readFile(fileInput: () -> FileInputStream) {
    var fileInputStream: FileInputStream? = null
    try {
        fileInputStream = fileInput()
        val reader = BufferedReader(InputStreamReader(fileInputStream))
        val stringBuilder = StringBuilder()
        reader.forEachLine { line -> stringBuilder.appendln(line) }
        result.text = stringBuilder.toString()
    } catch (e: Exception) {
        Log.e(TAG, "Error occurred when reading file", e)
    } finally {
        fileInputStream?.close()
    }
}

Чтобы зашифровать и прочитать скачанный файл, я вызвал:

1
readFile { encryptedFile.openFileInput() }

Чтобы убедиться, что данные нечитабельны без расшифровки, загрузим файл следующим образом:

1
readFile { file.inputStream() }

Образцы кода

Примеры кода из статьи можно найти в репозитории по ссылке.

Выводы

AndroidX Security ловко скрывает реализацию сложной логику безопасности, предоставляя разработчикам простые интерфейсы. С помощью этой библиотеки и нескольких строк кода можно сделать приложение безопаснее и избежать ситуации, когда разработчик забывает сконфигурировать что-то важное. Единственным недостатком является то, что разработанное приложение будет совместимо только с Android Marshmallow и более поздними версиями. Однако, учитывая ситуацию с KeyGenParameterSpec и KeyPairGeneratorSpec, это вполне разумный компромисс. Кроме того, согласно статистике от Google, всего четверть устройств в мире работают на Android 5 или более ранних версиях и их количество постоянно уменьшается. При этом не следует забывать, что шифрование SharedPreferences и файлов в проекте — лишь один из многих факторов, делающих приложение по-настоящему безопасным. Будем надеяться, что стабильная версия библиотеки будет выпущена Google в ближайшее время.

Оригинал статьи: тут

Запросить консультацию