· 6 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}