· 6 years ago · Jun 01, 2019, 06:32 PM
1package ru.zenit.android.interactor.auth
2
3import android.annotation.TargetApi
4import android.content.SharedPreferences
5import android.hardware.fingerprint.FingerprintManager
6import android.os.Build
7import android.security.keystore.KeyGenParameterSpec
8import android.security.keystore.KeyProperties
9import android.util.Base64
10import ru.surfstudio.android.dagger.scope.PerApplication
11import ru.surfstudio.android.logger.Logger
12import ru.surfstudio.android.shared.pref.NO_BACKUP_SHARED_PREF
13import ru.surfstudio.android.shared.pref.SettingsUtil
14import ru.zenit.android.interactor.auth.network.error.AUTH_PIN_TRY_LIMIT
15import ru.zenit.android.interactor.auth.network.error.AuthPinFailedException
16import ru.zenit.android.interactor.auth.network.error.AuthPinLimitReachedException
17import java.security.InvalidKeyException
18import java.security.KeyStore
19import java.security.SecureRandom
20import javax.crypto.*
21import javax.crypto.spec.IvParameterSpec
22import javax.crypto.spec.PBEKeySpec
23import javax.inject.Inject
24import javax.inject.Named
25
26/**
27 * Защищенное хранилище, используется AES шифрование
28 */
29@PerApplication
30class SecureStorage
31@Inject constructor(@Named(NO_BACKUP_SHARED_PREF) private val noBackupSharedPref: SharedPreferences) {
32 companion object {
33 private const val KEY_AUTH_PIN_TRY_COUNT = "auth_pin_try_count"
34
35 private const val KEY_JWT_TOKEN_BY_PIN = "jwt_token_pin"
36 private const val KEY_JWT_TOKEN_BY_FINGERPRINT = "jwt_token_fngprnt"
37
38 private const val ALIAS_FINGERPRINT = "FNGRPRNT"
39
40 private const val CIPHER_TRANSFORMATION = "AES/CBC/PKCS7Padding"
41 private const val KEY_ALGORITHM = "PBKDF2WithHmacSHA1"
42
43 private const val ANDROID_KEYSTORE = "AndroidKeyStore"
44
45 private const val SALT_SIZE = 16
46 private const val KEY_LENGTH = 256
47 private const val ITERATION_COUNT = 16384
48 }
49
50 fun saveJwtToken(jwtToken: String, pin: String, cryptoObject: FingerprintManager.CryptoObject?): Boolean = try {
51 val salt = generateSalt()
52 val spec = PBEKeySpec(pin.toCharArray(), salt, ITERATION_COUNT, KEY_LENGTH)
53
54 val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION)
55 cipher.init(Cipher.ENCRYPT_MODE, SecretKeyFactory.getInstance(KEY_ALGORITHM).generateSecret(spec))
56
57 val secretValue = SecretValue(cipher.doFinal(jwtToken.toByteArray()), cipher.iv, salt)
58 SettingsUtil.putString(noBackupSharedPref, KEY_JWT_TOKEN_BY_PIN, secretValue.toString())
59
60 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && cryptoObject != null) {
61 SettingsUtil.putString(noBackupSharedPref, KEY_JWT_TOKEN_BY_FINGERPRINT,
62 SecretValue(cryptoObject.cipher.doFinal(jwtToken.toByteArray()), cryptoObject.cipher.iv, generateSalt()).toString())
63 }
64
65 clearAuthPinTryCount()
66 true
67 } catch (e: Throwable) {
68 Logger.e("An exception occurred during saveJwtToken: $e", e)
69 false
70 }
71
72 fun getJwtToken(pin: String): String = try {
73 val secretValue = SecretValue.fromString(SettingsUtil.getString(noBackupSharedPref, KEY_JWT_TOKEN_BY_PIN))
74 val spec = PBEKeySpec(pin.toCharArray(), secretValue.salt, ITERATION_COUNT, KEY_LENGTH)
75
76 val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION)
77 cipher.init(Cipher.DECRYPT_MODE, SecretKeyFactory.getInstance(KEY_ALGORITHM).generateSecret(spec),
78 IvParameterSpec(secretValue.iv))
79
80 val result = String(cipher.doFinal(secretValue.secret))
81 clearAuthPinTryCount()
82 result
83
84 } catch (e: Throwable) {
85 Logger.e("An exception occurred during getJwtToken: $e", e)
86 if (e is InvalidKeyException || e is BadPaddingException) {
87 val currentCount = incrementAuthPinTryCount()
88 if (currentCount >= AUTH_PIN_TRY_LIMIT) {
89 throw AuthPinLimitReachedException(currentCount)
90 } else {
91 throw AuthPinFailedException(currentCount)
92 }
93 }
94 throw SecurityException(e)
95 }
96
97 @TargetApi(Build.VERSION_CODES.M)
98 fun getJwtToken(cryptoObject: FingerprintManager.CryptoObject): String = try {
99 val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
100 keyStore.load(null)
101
102 val key = keyStore.getKey(ALIAS_FINGERPRINT, null) as SecretKey
103 val secretValue = SecretValue.fromString(SettingsUtil.getString(noBackupSharedPref, KEY_JWT_TOKEN_BY_FINGERPRINT))
104
105 val cipher = cryptoObject.cipher
106 cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(secretValue.iv))
107
108 String(cipher.doFinal(secretValue.secret))
109 } catch (e: Throwable) {
110 Logger.e("An exception occurred during getJwtToken: $e", e)
111 throw SecurityException(e)
112 }
113
114 fun clear() {
115 SettingsUtil.putInt(noBackupSharedPref, KEY_AUTH_PIN_TRY_COUNT, 0)
116 SettingsUtil.putString(noBackupSharedPref, KEY_JWT_TOKEN_BY_PIN, SettingsUtil.EMPTY_STRING_SETTING)
117 SettingsUtil.putString(noBackupSharedPref, KEY_JWT_TOKEN_BY_FINGERPRINT, SettingsUtil.EMPTY_STRING_SETTING)
118 }
119
120 private fun getAuthPinTryCount() = SettingsUtil.getInt(noBackupSharedPref, KEY_AUTH_PIN_TRY_COUNT)
121
122 private fun incrementAuthPinTryCount() : Int {
123 var count = getAuthPinTryCount()
124 count++
125 setAuthPinTryCount(count)
126 return count
127 }
128
129 private fun clearAuthPinTryCount() {
130 setAuthPinTryCount(0)
131 }
132
133 private fun setAuthPinTryCount(count: Int) {
134 SettingsUtil.putInt(noBackupSharedPref, KEY_AUTH_PIN_TRY_COUNT, count)
135 }
136
137 @TargetApi(Build.VERSION_CODES.M)
138 fun prepareFingerprintCryptoObject(createKey: Boolean): FingerprintManager.CryptoObject {
139 val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
140 keyStore.load(null)
141
142 val key = if (createKey) {
143 createFingerprintKey(ALIAS_FINGERPRINT, keyStore)
144 } else {
145 keyStore.getKey(ALIAS_FINGERPRINT, null) as SecretKey?
146 }
147
148 val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION)
149 cipher.init(Cipher.ENCRYPT_MODE, key)
150 return FingerprintManager.CryptoObject(cipher)
151 }
152
153 @TargetApi(Build.VERSION_CODES.M)
154 private fun createFingerprintKey(alias: String, keystore: KeyStore): SecretKey? {
155 val builder = KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
156 .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
157 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
158 .setUserAuthenticationRequired(false)
159 .setUserAuthenticationValidityDurationSeconds(-1)
160
161 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
162 builder.setInvalidatedByBiometricEnrollment(false)
163 builder.setUserAuthenticationValidWhileOnBody(false)
164 }
165
166 val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, keystore.provider)
167 keyGenerator.init(builder.build())
168 return keyGenerator.generateKey()
169 }
170
171 private fun generateSalt(): ByteArray {
172 val salt = ByteArray(SALT_SIZE)
173 SecureRandom().nextBytes(salt)
174 return salt
175 }
176
177 private class SecretValue(val secret: ByteArray, val iv: ByteArray, val salt: ByteArray) {
178 companion object {
179 private const val DELIMETER = "["
180
181 fun fromString(value: String): SecretValue {
182 val split = value.split(DELIMETER)
183 if (split.size != 3) throw IllegalArgumentException()
184
185 return SecretValue(Base64.decode(split[2], Base64.NO_WRAP),
186 Base64.decode(split[0], Base64.NO_WRAP),
187 Base64.decode(split[1], Base64.NO_WRAP))
188 }
189 }
190
191 override fun toString(): String {
192 return Base64.encodeToString(iv, Base64.NO_WRAP) + DELIMETER +
193 Base64.encodeToString(salt, Base64.NO_WRAP) + DELIMETER +
194 Base64.encodeToString(secret, Base64.NO_WRAP)
195 }
196 }
197}