· 5 years ago · Dec 18, 2019, 10:20 AM
1package ru.sbi.android.i_security.storage.secure
2
3import android.annotation.TargetApi
4import android.content.Context
5import android.content.SharedPreferences
6import android.hardware.fingerprint.FingerprintManager
7import android.os.Build
8import android.security.KeyPairGeneratorSpec
9import android.security.keystore.KeyGenParameterSpec
10import android.security.keystore.KeyProperties
11import androidx.annotation.RequiresApi
12import org.threeten.bp.LocalDate
13import ru.sbi.android.i_security.crypto.security.SecurityUtils.getRandomBytes
14import ru.sbi.android.i_security.crypto.security.initDecryptMode
15import ru.sbi.android.i_security.crypto.security.initEncryptMode
16import ru.sbi.android.i_security.storage.*
17import ru.sbi.android.util.date.convertToDate
18import ru.surfstudio.android.dagger.scope.PerApplication
19import ru.surfstudio.android.shared.pref.NO_BACKUP_SHARED_PREF
20import ru.surfstudio.android.shared.pref.SettingsUtil
21import ru.surfstudio.android.utilktx.ktx.text.EMPTY_STRING
22import ru.surfstudio.android.utilktx.util.SdkUtils
23import java.math.BigInteger
24import java.nio.charset.StandardCharsets
25import java.security.*
26import java.security.cert.Certificate
27import java.security.cert.CertificateFactory
28import javax.crypto.Cipher
29import javax.crypto.KeyGenerator
30import javax.crypto.SecretKey
31import javax.crypto.spec.IvParameterSpec
32import javax.inject.Inject
33import javax.inject.Named
34import javax.security.auth.x500.X500Principal
35
36private const val ANDROID_KEYSTORE = "AndroidKeyStore"
37private const val KEY_ALGORITHM_RSA = "RSA"
38private const val AES_CIPHER_TRANSFORMATION = "AES/CBC/PKCS7Padding"
39private const val RSA_CIPHER_TRANSFORMATION = "RSA/ECB/PKCS1Padding"
40
41private const val KEY_LENGTH_AES = 256
42private const val KEY_LENGTH_RSA = 2048
43private const val IV_SIZE_IN_BYTES = 16
44
45/**
46 * Безопасное хранилище для произвольных строк.
47 * Здесь должны храниться токены, пароли, пины и другие данные, компрометация которых нежелательна.
48 * Шифрование выполняется с помошью AES-256 в режиме CBC или RSA-2048 в режиме ECB.
49 * Последний используется для Api < 23.
50 * Шифрованные данные записываются в  [SharedPreferences], а сгенерированные ключи - в [KeyStore].
51 */
52@PerApplication
53class SecureStorage @Inject constructor(
54        @Named(NO_BACKUP_SHARED_PREF) private val sharedPreferences: SharedPreferences,
55        private val fingerprintStubDataStorage: FingerprintStubDataStorage,
56        private val context: Context
57) : BaseSecureStorage {
58
59    override fun load(alias: String, loadAsIs: Boolean): String? {
60        return if (SdkUtils.isAtLeastMarshmallow()) {
61            loadWithSymmetricCipher(alias, loadAsIs)
62        } else {
63            loadWithAsymmetricCypher(alias, loadAsIs)
64        }
65    }
66
67    override fun save(alias: String, data: String, saveAsIs: Boolean) {
68        if (SdkUtils.isAtLeastMarshmallow()) {
69            saveWithSymmetricCipher(alias, data, saveAsIs)
70        } else {
71            saveWithAsymmetricCipher(alias, data, saveAsIs)
72        }
73    }
74
75    /**
76     * Функция, проверяющая введенный пользователем пин-код на валидность
77     */
78    fun isPinValid(pinFromInput: String): Boolean =
79            pinFromInput == load(PIN_CODE)
80
81    /**
82     * Заменить строку по алиасу. Сохраняет новое значение только при наличии старого
83     *
84     * @param saveAsIs true для сохранения без шифрования - "как есть"
85     */
86    fun replace(alias: String, data: String, saveAsIs: Boolean = false) {
87        load(alias)?.also {
88            save(alias, data, saveAsIs)
89        }
90    }
91
92    /**
93     * Функция, возвращающая зашифрованные данные с помощью симметричного шифрования
94     *
95     * @param alias алиас для генерации ключа
96     * @param data данные, которые требуется зашифровать
97     */
98    private fun encryptWithSymmetricCipher(alias: String, data: String): String {
99        val keyStore = getKeyStore()
100
101        val ivBytes = getRandomBytes(IV_SIZE_IN_BYTES)
102        val iv = IvParameterSpec(ivBytes)
103
104        val key: Key = if (keyStore.containsAlias(alias)) {
105            keyStore.getKey(alias, null)
106        } else {
107            val generatedKey = generateSecretKey(alias, keyStore)
108            keyStore.setKeyEntry(alias, generatedKey, null, arrayOf())
109            generatedKey
110        }
111
112        val cipher = Cipher.getInstance(AES_CIPHER_TRANSFORMATION)
113                .apply {
114                    init(Cipher.ENCRYPT_MODE, key, iv)
115                }
116        return (ivBytes + cipher.doFinal(data.toBytes())).encodeStringForPreferences()
117    }
118
119    /**
120     * Функция, возвращающая расшифрованные по алиасу данные с помощью симметричного шифрования
121     *
122     * @param alias алиас, по которому был создан ключ шифрования
123     * @param encryptedData зашифрованные данные
124     */
125    private fun decryptWithSymmetricCipher(alias: String, encryptedData: String): String? {
126        var preferencesBytes = encryptedData.decodeStringFromPreferences()
127
128        val ivBytes = preferencesBytes.take(IV_SIZE_IN_BYTES).toByteArray()
129        val iv = IvParameterSpec(ivBytes)
130
131        preferencesBytes = preferencesBytes.drop(IV_SIZE_IN_BYTES).toByteArray()
132
133        val keyStore = getKeyStore()
134        if (!keyStore.containsAlias(alias)) return null
135        val key = keyStore.getKey(alias, null)
136
137        with(Cipher.getInstance(AES_CIPHER_TRANSFORMATION)) {
138            init(Cipher.DECRYPT_MODE, key, iv)
139            return doFinal(preferencesBytes).toUtf8String()
140        }
141    }
142
143    //region save
144    /**
145     * Сохраняет произвольную строку в настройки, предварительно шифруя ее с помощью ключа.
146     * Последний используемый ключ сохраняется в KeyStore. Шифрование AES.
147     *
148     * Замечание: в начале данных хранится вектор инициализации размером [IV_SIZE_IN_BYTES]
149     */
150    private fun saveWithSymmetricCipher(alias: String, data: String, saveAsIs: Boolean) {
151        if (saveAsIs) {
152            SettingsUtil.putString(sharedPreferences, alias, data)
153            return
154        }
155        SettingsUtil.putString(sharedPreferences, alias, encryptWithSymmetricCipher(alias, data))
156    }
157
158    /**
159     * Сохраняет произвольную строку в настройки, предварительно шифруя ее с помощью ключа.
160     * Последний используемый ключ сохраняется в KeyStore. Шифрование RSA.
161     *
162     * Замечания:
163     * 1. Для каждого нового сохранения используется новая пара ключей и публичный при этом
164     * никуда не сохраняем. Это является ограничением KeyStore, а простого/известного
165     * обходного пути - нет.
166     * 2. Сертификат для приватного ключа сгенерирован через openssl и нужен только чтобы соблюсти
167     * синтаксис. Хранится в asset'ах.
168     *
169     * @param saveAsIs true если хотим сохранить данные без шифрования
170     */
171    private fun saveWithAsymmetricCipher(alias: String, data: String, saveAsIs: Boolean) {
172        if (saveAsIs) {
173            SettingsUtil.putString(sharedPreferences, alias, data)
174            return
175        }
176
177        val keyStore = getKeyStore()
178        val cipher = Cipher.getInstance(RSA_CIPHER_TRANSFORMATION)
179
180        val generatedKeyPair = generateKeyPair(alias, keyStore)
181        val privateKey = generatedKeyPair.private
182        val publicKey = generatedKeyPair.public
183
184        val fileName = "private_key_cert.cert"
185
186        val certificateInputStream = context.resources.assets.open(fileName)
187        val certificateFactory = CertificateFactory.getInstance("X.509")
188        val certificate: Certificate = certificateFactory.generateCertificate(certificateInputStream)
189
190        keyStore.setKeyEntry(alias, privateKey, null, arrayOf(certificate))
191
192        cipher.init(Cipher.ENCRYPT_MODE, publicKey)
193
194        val encryptedBytes = cipher.doFinal(data.toBytes())
195        val toSave = encryptedBytes.encodeStringForPreferences()
196
197        SettingsUtil.putString(sharedPreferences, alias, toSave)
198    }
199    //endregion
200
201    //region load
202    /**
203     * Извлекает произвольную строку из настроек, расшифровывая ее существующим симметричным ключом
204     *
205     * Замечание: в начале данных хранится вектор инициализации размером [IV_SIZE_IN_BYTES]
206     */
207    private fun loadWithSymmetricCipher(alias: String, loadAsIs: Boolean): String? {
208        SettingsUtil.getString(sharedPreferences, alias).also { preferencesString ->
209            return when {
210                preferencesString.isBlank() -> null
211                loadAsIs -> preferencesString
212                else -> decryptWithSymmetricCipher(alias, preferencesString)
213            }
214        }
215    }
216
217    /**
218     *  Извлекает произвольную строку из настроек, расшифровывая ее существующим приватным ключом
219     *
220     *  @param loadAsIs true если не нужно расшифровывать (шифрование перед сохранением не использовалось)
221     */
222    private fun loadWithAsymmetricCypher(alias: String, loadAsIs: Boolean): String? {
223        val preferencesString = SettingsUtil.getString(sharedPreferences, alias)
224        if (preferencesString.isBlank()) return null
225
226        if (loadAsIs) {
227            return preferencesString
228        }
229
230        val preferencesBytes = preferencesString.decodeStringFromPreferences()
231        val keyStore = getKeyStore()
232        val key = keyStore.getKey(alias, null)
233
234        with(Cipher.getInstance(RSA_CIPHER_TRANSFORMATION)) {
235            init(Cipher.DECRYPT_MODE, key)
236            return doFinal(preferencesBytes).toUtf8String()
237        }
238    }
239    //endregion
240
241    /**
242     * Подготовка крипто-контейнера.
243     *
244     * @param shouldCreateKey флаг необходимости генерации криптоключа
245     * @param isEncryptMode используемый режим: шифрование или дешифрование
246     * @param alias алиас, по которому будут сохранены данные
247     *
248     * * true - ключ генерируется и сохраняется в [KeyStore] (отпечаток пальца запрашивается в
249     * первый раз, сценарий регистрации);
250     * * false - ключ не генерируется, а извлекается из [KeyStore] (отпечаток пальца
251     * предположительно уже был сохранён, сценарий авторизации);
252     *
253     * Замечание: на случай, если отпечаток пальца был зарегистрирован в системе позже получения данных,
254     * внутри предусмотрена логика повторного шифрования с "новым" ключом
255     */
256    @TargetApi(Build.VERSION_CODES.M)
257    fun prepareFingerprintCryptoObject(
258            shouldCreateKey: Boolean,
259            isEncryptMode: Boolean,
260            alias: String
261    ): FingerprintManager.CryptoObject {
262        val keyStore = getKeyStore()
263        val key = if (shouldCreateKey) {
264            val alreadySavedString = load(alias)
265
266            val fingerprintKey = createFingerprintKey(alias, keyStore, true).apply {
267                keyStore.setKeyEntry(alias, this, null, arrayOf())
268            }
269
270            if (!alreadySavedString.isNullOrEmpty()) {
271                save(alias, alreadySavedString)
272            }
273
274            fingerprintKey
275        } else {
276            keyStore.getKey(alias, null) as SecretKey
277        }
278
279        val cipher = Cipher.getInstance(AES_CIPHER_TRANSFORMATION).apply {
280            if (isEncryptMode) {
281                initEncryptMode(key, getRandomBytes(IV_SIZE_IN_BYTES))
282            } else {
283                val preferencesString = SettingsUtil.getString(sharedPreferences, alias)
284                val preferencesBytes = preferencesString.decodeStringFromPreferences()
285                val ivBytes = preferencesBytes.take(IV_SIZE_IN_BYTES).toByteArray()
286                initDecryptMode(key, ivBytes)
287            }
288        }
289        return FingerprintManager.CryptoObject(cipher)
290    }
291
292    /**
293     * Сохранение вспомогательного [FingerprintManager.CryptoObject]
294     * для возможности дальнейшей проверки изменения состава отпечатков пальца,
295     * зарегистрированных на девайсе.
296     */
297    @RequiresApi(Build.VERSION_CODES.M)
298    fun saveFingerprintStub(cryptoObject: FingerprintManager.CryptoObject) {
299        if (!fingerprintStubDataStorage.hasFingerprintStub) {
300            val encryptedBytes = cryptoObject.cipher.doFinal(FINGERPRINT_STUB_CHECK.toBytes())
301            val toSave = (cryptoObject.cipher.iv + encryptedBytes).encodeStringForPreferences()
302            fingerprintStubDataStorage.fingerprintStub = toSave
303        }
304    }
305
306    //region clear
307    /**
308     * Функция, очищающая хранилище по всем его алиасам
309     */
310    fun clear(clearSslStorage: Boolean = true, clearAuthStorage: Boolean = true) {
311        for (alias in aliases) {
312            if (!clearSslStorage && alias in sslAliases) {
313                continue
314            }
315            if (!clearAuthStorage && alias in authAliases) {
316                continue
317            }
318            deleteByAlias(alias)
319        }
320    }
321
322    /**
323     * Функция, очищающая хранилище по конкретным алиасам
324     */
325    fun clear(aliases: List<String>) {
326        aliases.forEach { alias ->
327            deleteByAlias(alias)
328        }
329    }
330
331    /**
332     * Удаляет шифрованную строку и соответствующий ей ключ шифрования в KeyStore
333     *
334     * @param alias алиас ключа/строки
335     */
336    private fun deleteByAlias(alias: String) {
337        getKeyStore().deleteEntry(alias)
338        SettingsUtil.putString(sharedPreferences, alias, EMPTY_STRING)
339    }
340    //endregion
341
342    @TargetApi(Build.VERSION_CODES.M)
343    private fun generateSecretKey(alias: String, keyStore: KeyStore): SecretKey {
344        val builder = getKeySpecBuilder(alias)
345        val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, keyStore.provider)
346                .apply {
347                    init(builder.build(), SecureRandom())
348                }
349        return keyGenerator.generateKey()
350    }
351
352    private fun generateKeyPair(alias: String, keyStore: KeyStore): KeyPair {
353        val builder = getKeyPairSpecBuilder(alias)
354        val keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM_RSA, keyStore.provider)
355                .apply {
356                    initialize(builder.build())
357                }
358        return keyPairGenerator.generateKeyPair()
359    }
360
361    /**
362     * Возвращает билдер с настройками генерации симметричного ключа
363     */
364    @TargetApi(Build.VERSION_CODES.M)
365    private fun getKeySpecBuilder(keyAlias: String): KeyGenParameterSpec.Builder {
366        return KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
367                .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
368                .setKeySize(KEY_LENGTH_AES)
369                .setRandomizedEncryptionRequired(false)
370                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
371                .setUserAuthenticationRequired(false)
372                .setDigests(KeyProperties.DIGEST_NONE)
373                .setUserAuthenticationValidityDurationSeconds(-1)
374    }
375
376    /**
377     * Возвращает билдер с настройками генерации пары public/private ключей
378     *
379     * Замечание: Параметры сертификата в билдере нужны только для соблюдения синтаксиса.
380     * Так как ключи не должны экспортироваться из устройства, вопрос доверия к публичному ключу
381     * не стоит.
382     */
383    @SuppressWarnings("deprecation")
384    private fun getKeyPairSpecBuilder(keyAlias: String): KeyPairGeneratorSpec.Builder {
385        val start = LocalDate.now()
386        val end = LocalDate.now().plusYears(30)
387
388        return KeyPairGeneratorSpec.Builder(context)
389                .setSubject(X500Principal("CN=$keyAlias"))
390                .setSerialNumber(BigInteger.TEN)
391                .setStartDate(start.convertToDate())
392                .setEndDate(end.convertToDate())
393                .setAlias(keyAlias)
394                .setKeySize(KEY_LENGTH_RSA)
395    }
396
397    private fun String.toBytes(): ByteArray {
398        return this.toByteArray(StandardCharsets.UTF_8)
399    }
400
401    /**
402     * Конвертирует строку вида "[1, -2, -38, 41 ...]" в массив байтов с соответствующими значениями
403     */
404    private fun String.decodeStringFromPreferences(): ByteArray {
405        val split = substring(1, length - 1).split(", ")
406        val array = ByteArray(split.size)
407        for (i in split.indices) {
408            array[i] = java.lang.Byte.parseByte(split[i])
409        }
410        return array
411    }
412
413    /**
414     * Кодирует массив байтов как строку вида "[1, -2, -38, 41 ...]"
415     */
416    private fun ByteArray.encodeStringForPreferences(): String {
417        return this.contentToString()
418    }
419
420    private fun ByteArray.toUtf8String(): String {
421        return String(this)
422    }
423
424    private fun getKeyStore(): KeyStore {
425        return KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
426    }
427
428    /**
429     * Генерация крипто-контейнера.
430     *
431     * @param alias алиас для создания ключа
432     * @param keystore экземпляр [KeyStore]
433     * @param isUserAuthenticationRequired нужно ли использовать опцию setUserAuthenticationRequired билдера.
434     * Опция используется только при использовании [prepareFingerprintCryptoObject]
435     * для сохранения данных-заглушки и дальнейшей проверки, что на девайсе не менялся состав
436     * зарегистрированных отпечатков.
437     */
438    @TargetApi(Build.VERSION_CODES.M)
439    private fun createFingerprintKey(
440            alias: String,
441            keystore: KeyStore,
442            isUserAuthenticationRequired: Boolean
443    ): SecretKey {
444        val builder = getKeySpecBuilder(alias)
445                .apply {
446                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
447                        setInvalidatedByBiometricEnrollment(true)
448                        setUserAuthenticationValidWhileOnBody(false)
449                        setUserAuthenticationRequired(isUserAuthenticationRequired)
450                    }
451                }
452        val keyGenerator = KeyGenerator.getInstance(
453                KeyProperties.KEY_ALGORITHM_AES,
454                keystore.provider
455        ).apply {
456            init(builder.build())
457        }
458        return keyGenerator.generateKey()
459    }
460}