Skip to content
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

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ OTUS Репозиторий домашней работы по безопасн
###### _Дополнительно_
4. Добавьте возможность включать или отключать авторизацию через биометрию

##### _Тестовые пользователь_

Email: [email protected]
Пароль: otus

##### Материаы, которыми можно пользоваться:

[otus_security](https://github.com/vitalyraevsky/otus_security)
Expand Down
3 changes: 2 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ dependencies {
implementation "androidx.navigation:navigation-fragment-ktx:2.3.5"
implementation "androidx.navigation:navigation-ui-ktx:2.3.5"
implementation "com.google.dagger:hilt-android:2.38.1"
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03'
implementation "androidx.security:security-crypto:1.1.0-alpha03"
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha03"
kapt 'androidx.hilt:hilt-compiler:1.0.0'
kapt "com.google.dagger:hilt-android-compiler:2.38.1"
}
12 changes: 12 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,22 @@
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:name=".App"
android:supportsRtl="true"
android:theme="@style/Theme.SecureHomeWork">

<activity
android:name=".presentation.splash.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".presentation.home.HomeActivity" />
<activity android:name=".presentation.auth.AuthActivity" />
</application>

</manifest>
Binary file added app/src/main/ic_launcher-playstore.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

в идеале пароль поменять из String на CharSequence

companion object {
val STUB = LoginData("", "")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ package com.otus.securehomework.data.repository

import com.otus.securehomework.data.Response
import com.otus.securehomework.data.dto.LoginResponse
import com.otus.securehomework.data.source.local.UserPreferences
import com.otus.securehomework.data.source.network.AuthApi
import javax.inject.Inject

class AuthRepository
@Inject constructor(
private val api: AuthApi,
private val preferences: UserPreferences
private val api: AuthApi
) : BaseRepository(api) {

suspend fun login(
Expand All @@ -18,8 +16,4 @@ class AuthRepository
): Response<LoginResponse> {
return safeApiCall { api.login(email, password) }
}

suspend fun saveAccessTokens(accessToken: String, refreshToken: String) {
preferences.saveAccessTokens(accessToken, refreshToken)
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package com.otus.securehomework.data.repository

import android.content.Context
import com.otus.securehomework.data.dto.TokenResponse
import com.otus.securehomework.data.source.local.UserPreferences
import com.otus.securehomework.data.source.local.SecureUserPreferences
import com.otus.securehomework.data.source.network.TokenRefreshApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
Expand All @@ -13,19 +12,16 @@ import okhttp3.Route
import javax.inject.Inject
import com.otus.securehomework.data.Response as DataResponse


class TokenAuthenticator @Inject constructor(
context: Context,
private val tokenApi: TokenRefreshApi
class TokenAuthenticator(
private val tokenApi: TokenRefreshApi,
private val preferences: SecureUserPreferences
) : Authenticator, BaseRepository(tokenApi) {

private val userPreferences = UserPreferences(context)

override fun authenticate(route: Route?, response: Response): Request? {
return runBlocking {
when (val tokenResponse = getUpdatedToken()) {
is DataResponse.Success -> {
userPreferences.saveAccessTokens(
preferences.saveAccessTokens(
tokenResponse.value.access_token,
tokenResponse.value.refresh_token
)
Expand All @@ -39,7 +35,7 @@ class TokenAuthenticator @Inject constructor(
}

private suspend fun getUpdatedToken(): DataResponse<TokenResponse> {
val refreshToken = userPreferences.refreshToken.first()
val refreshToken = preferences.refreshToken.first()
return safeApiCall {
tokenApi.refreshAccessToken(refreshToken)
}
Expand Down
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)

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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
}
}
Loading