-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/secure auth #1
base: master
Are you sure you want to change the base?
Changes from all commits
e851a81
1f6832d
1620faf
7c77caa
39a77f3
8457ffc
ac4f8e2
0f7c58a
334a0a2
94a9f3a
defdb15
56c2e69
8970a55
46eea28
81c643c
51fda0e
7c3721b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,11 @@ OTUS Репозиторий домашней работы по безопасн | |
###### _Дополнительно_ | ||
4. Добавьте возможность включать или отключать авторизацию через биометрию | ||
|
||
##### _Тестовые пользователь_ | ||
|
||
Email: [email protected] | ||
Пароль: otus | ||
|
||
##### Материаы, которыми можно пользоваться: | ||
|
||
[otus_security](https://github.com/vitalyraevsky/otus_security) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.otus.securehomework.data.dto | ||
|
||
data class LoginData(val email: String, val password: String) { | ||
companion object { | ||
val STUB = LoginData("", "") | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package com.otus.securehomework.data.source.crypto | ||
|
||
import android.content.Context | ||
import android.os.Build | ||
import android.util.Base64 | ||
import androidx.annotation.RequiresApi | ||
import androidx.biometric.BiometricManager | ||
import androidx.biometric.auth.AuthPromptHost | ||
import androidx.biometric.auth.Class2BiometricAuthPrompt | ||
import androidx.biometric.auth.Class3BiometricAuthPrompt | ||
import androidx.biometric.auth.authenticate | ||
import com.otus.securehomework.R | ||
import com.otus.securehomework.data.source.local.SecureUserPreferences | ||
import dagger.hilt.android.qualifiers.ApplicationContext | ||
import javax.inject.Inject | ||
|
||
class BiometricAuthManager @Inject constructor( | ||
@ApplicationContext private val context: Context, | ||
private val biometricCipher: BiometricCipher, | ||
private val userPreferences: SecureUserPreferences | ||
) { | ||
|
||
suspend fun saveBiometricAuth(host: AuthPromptHost) { | ||
userPreferences.saveHasBiometrics(getBiometricData(host)) | ||
} | ||
|
||
suspend fun checkBiometricAuth(host: AuthPromptHost): Boolean { | ||
return getBiometricData(host) | ||
} | ||
|
||
private suspend fun getBiometricData(host: AuthPromptHost) = | ||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) { | ||
strongBiometricAuth(host) | ||
} else if (canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) { | ||
weakBiometricAuth(host) | ||
} else false | ||
|
||
@RequiresApi(Build.VERSION_CODES.M) | ||
private suspend fun strongBiometricAuth(host: AuthPromptHost): Boolean { | ||
Class3BiometricAuthPrompt.Builder("Strong biometry", "dismiss") | ||
.setSubtitle(context.getString(R.string.fingerprint_subtitle)) | ||
.setDescription(context.getString(R.string.fingerprint_description)) | ||
.setConfirmationRequired(true) | ||
.build() | ||
.auth(host, biometricCipher.getEncryptor()) | ||
return true | ||
} | ||
|
||
private suspend fun weakBiometricAuth(host: AuthPromptHost): Boolean { | ||
Class2BiometricAuthPrompt.Builder("Weak biometry", "dismiss") | ||
.setSubtitle(context.getString(R.string.fingerprint_subtitle)) | ||
.setDescription(context.getString(R.string.fingerprint_description)) | ||
.setConfirmationRequired(true) | ||
.build() | ||
.auth(host) | ||
return true | ||
} | ||
|
||
private fun canAuthenticate(authenticator: Int) = BiometricManager.from(context) | ||
.canAuthenticate(authenticator) == BiometricManager.BIOMETRIC_SUCCESS | ||
|
||
fun ByteArray.toBase64(flags: Int = Base64.DEFAULT): String = Base64.encodeToString(this, flags) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Можно эти экстеншены вынести в общие , так как будут переиспользоваться |
||
fun String.fromBase64(flags: Int = Base64.DEFAULT): ByteArray = Base64.decode(this, flags) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
package com.otus.securehomework.data.source.crypto | ||
|
||
import android.content.Context | ||
import android.content.pm.PackageManager | ||
import android.os.Build | ||
import android.security.keystore.KeyGenParameterSpec | ||
import android.security.keystore.KeyProperties | ||
import android.util.Log | ||
import androidx.annotation.RequiresApi | ||
import androidx.biometric.BiometricPrompt | ||
import dagger.hilt.android.qualifiers.ApplicationContext | ||
import java.security.KeyStore | ||
import javax.crypto.Cipher | ||
import javax.crypto.KeyGenerator | ||
import javax.crypto.SecretKey | ||
import javax.crypto.spec.GCMParameterSpec | ||
import javax.inject.Inject | ||
|
||
|
||
class BiometricCipher @Inject constructor( | ||
@ApplicationContext private val applicationContext: Context | ||
) { | ||
private val keyAlias by lazy { "${applicationContext.packageName}.biometricKey" } | ||
|
||
@RequiresApi(Build.VERSION_CODES.M) | ||
fun getEncryptor(): BiometricPrompt.CryptoObject = BiometricPrompt.CryptoObject( | ||
Cipher.getInstance(TRANSFORMATION) | ||
.apply { init(Cipher.ENCRYPT_MODE, getOrCreateKey()) } | ||
) | ||
|
||
@RequiresApi(Build.VERSION_CODES.M) | ||
fun getDecryptor(iv: ByteArray): BiometricPrompt.CryptoObject { | ||
return BiometricPrompt.CryptoObject( | ||
Cipher.getInstance(TRANSFORMATION) | ||
.apply { | ||
init( | ||
Cipher.DECRYPT_MODE, | ||
getOrCreateKey(), | ||
GCMParameterSpec(AUTH_TAG_SIZE, iv) | ||
) | ||
} | ||
) | ||
} | ||
|
||
fun encrypt(plaintext: String, encryptor: Cipher): EncryptedEntity { | ||
require(plaintext.isNotEmpty()) { "Plaintext cannot be empty" } | ||
val ciphertext = encryptor.doFinal(plaintext.toByteArray()) | ||
return EncryptedEntity( | ||
ciphertext, | ||
encryptor.iv | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. мне кажется странным, возвращать вектор как результат . Класс , который отвечает за шифрование, должен уметь и сохранять вектор безопасно |
||
) | ||
} | ||
|
||
fun decrypt(ciphertext: ByteArray, decryptor: Cipher): String { | ||
val plaintext = decryptor.doFinal(ciphertext) | ||
return String(plaintext, Charsets.UTF_8) | ||
} | ||
|
||
@RequiresApi(Build.VERSION_CODES.M) | ||
private fun getOrCreateKey(): SecretKey { | ||
val keystore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) } | ||
|
||
keystore.getKey(keyAlias, null)?.let { return it as SecretKey } | ||
|
||
val keySpec = KeyGenParameterSpec.Builder( | ||
keyAlias, | ||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT | ||
) | ||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM) | ||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) | ||
.setKeySize(KEY_SIZE) | ||
.setUserAuthenticationRequired(true) | ||
.apply { | ||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { | ||
setUnlockedDeviceRequired(true) | ||
|
||
val hasStringBox = applicationContext | ||
.packageManager | ||
.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE) | ||
|
||
setIsStrongBoxBacked(hasStringBox) | ||
} | ||
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { | ||
setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG) | ||
} | ||
} | ||
.build() | ||
|
||
return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER) | ||
.apply { init(keySpec) } | ||
.generateKey() | ||
} | ||
|
||
data class EncryptedEntity( | ||
val ciphertext: ByteArray, | ||
val iv: ByteArray | ||
) | ||
|
||
companion object { | ||
private const val KEYSTORE_PROVIDER = "AndroidKeyStore" | ||
private const val AUTH_TAG_SIZE = 128 | ||
private const val KEY_SIZE = 256 | ||
|
||
private const val TRANSFORMATION = "${KeyProperties.KEY_ALGORITHM_AES}/" + | ||
"${KeyProperties.BLOCK_MODE_GCM}/" + | ||
"${KeyProperties.ENCRYPTION_PADDING_NONE}" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package com.otus.securehomework.data.source.crypto | ||
|
||
import android.content.Context | ||
import android.content.SharedPreferences | ||
import android.os.Build | ||
import android.security.keystore.KeyGenParameterSpec | ||
import android.security.keystore.KeyProperties | ||
import androidx.security.crypto.MasterKey | ||
import dagger.hilt.android.qualifiers.ApplicationContext | ||
import javax.inject.Inject | ||
import javax.inject.Named | ||
|
||
class Keys @Inject constructor( | ||
@ApplicationContext val context: Context | ||
) { | ||
|
||
fun getMasterKey() = MasterKey.Builder(context) | ||
.setSpecs() | ||
.build() | ||
|
||
private fun MasterKey.Builder.setSpecs(): MasterKey.Builder = | ||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | ||
setKeyGenParameterSpec( | ||
KeyGenParameterSpec.Builder( | ||
MasterKey.DEFAULT_MASTER_KEY_ALIAS, | ||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT | ||
) | ||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM) | ||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) | ||
.setKeySize(KEY_LENGTH) | ||
.build() | ||
) | ||
} else { | ||
setKeyScheme(MasterKey.KeyScheme.AES256_GCM) | ||
} | ||
|
||
companion object { | ||
const val KEY_LENGTH = 256 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
в идеале пароль поменять из String на CharSequence