@ -16,29 +16,120 @@ import android.os.Handler
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricPrompt
import java.util.concurrent.Executor
import android.util.Base64
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
//import javax.crypto.spec.IvParameterSpec
import java.nio.charset.Charset
import app.tauri.biometric.CipherType
class BiometricActivity : AppCompatActivity ( ) {
private var ENCRYPTION _ALGORITHM = KeyProperties . KEY _ALGORITHM _AES
private val ENCRYPTION _BLOCK _MODE = KeyProperties . BLOCK _MODE _GCM
private val ENCRYPTION _PADDING = KeyProperties . ENCRYPTION _PADDING _NONE
private val KEYSTORE _NAME = " AndroidKeyStore "
private val KEY _ALIAS = " tauri-plugin-biometric-key "
@SuppressLint ( " WrongConstant " )
override fun onCreate ( savedInstanceState : Bundle ? ) {
super . onCreate ( savedInstanceState )
setContentView ( R . layout . auth _activity )
val executor = if ( Build . VERSION . SDK _INT >= Build . VERSION_CODES . P ) {
this . mainExecutor
} else {
Executor { command : Runnable ? ->
Handler ( this . mainLooper ) . post (
command !!
val promptInfo = createPromptInfo ( )
val prompt = createBiometricPrompt ( )
cipherOperation = intent . hasExtra ( BiometricPlugin . ENCRYPT _DECRYPT _OPERATION )
if ( ! cipherOperation ) {
prompt . authenticate ( promptInfo )
return
}
intent . getStringExtra ( BiometricPlugin . ENCRYPT _DECRYPT _DATA ) ?. let {
encryptDecryptData = it
}
try {
val type = CipherType . values ( ) [ intent . getIntExtra ( BiometricPlugin . CIPHER _OPERATION _TYPE , CipherType . ENCRYPT . ordinal ) ]
cipherType = type
} catch ( e : ArrayIndexOutOfBoundsException ) {
finishActivity (
BiometryResultType . ERROR ,
0 ,
" Couldn't identify the cipher operation type (encrypt/decrypt)! "
)
return
}
val cipher = getCipher ( )
prompt . authenticate ( promptInfo , BiometricPrompt . CryptoObject ( cipher ) )
}
@JvmOverloads
fun finishActivity (
resultType : BiometryResultType = BiometryResultType . SUCCESS ,
errorCode : Int = 0 ,
errorMessage : String ? = " "
) {
val intent = Intent ( )
val prefix = BiometricPlugin . RESULT _EXTRA _PREFIX
intent
. putExtra ( prefix + BiometricPlugin . RESULT _TYPE , resultType . toString ( ) )
. putExtra ( prefix + BiometricPlugin . RESULT _ERROR _CODE , errorCode )
. putExtra (
prefix + BiometricPlugin . RESULT _ERROR _MESSAGE ,
errorMessage
)
. putExtra (
prefix + BiometricPlugin . ENCRYPT _DECRYPT _OPERATION ,
cipherOperation
)
if ( cipherOperation ) {
val cryptoObject = requireNotNull ( authenticationResult ?. cryptoObject )
val cipher = requireNotNull ( cryptoObject . cipher )
var dataToProcess = if ( cipherType == CipherType . ENCRYPT ) {
encryptDecryptData . toByteArray ( )
} else {
val ( _ , encryptedData ) = decodeEncryptedData ( encryptDecryptData )
encryptedData
}
var processedData : ByteArray = cipher . doFinal ( dataToProcess )
if ( cipherType == CipherType . ENCRYPT ) {
// Converts the encrypted data to Base64 string
val encodedString = encodeEncryptedData ( cipher , processedData )
intent . putExtra (
prefix + BiometricPlugin . RESULT _ENCRYPT _DECRYPT _DATA ,
encodedString
)
} else {
// For decryption, return the decrypted string
intent . putExtra (
prefix + BiometricPlugin . RESULT _ENCRYPT _DECRYPT _DATA ,
String ( processedData , Charset . forName ( " UTF-8 " ) )
)
}
}
setResult ( Activity . RESULT _OK , intent )
finish ( )
}
private fun createPromptInfo ( ) : BiometricPrompt . PromptInfo {
val builder = BiometricPrompt . PromptInfo . Builder ( )
val intent = intent
var title = intent . getStringExtra ( BiometricPlugin . TITLE )
val subtitle = intent . getStringExtra ( BiometricPlugin . SUBTITLE )
val description = intent . getStringExtra ( BiometricPlugin . REASON )
allowDeviceCredential = false
cipherOperation = intent . hasExtra ( BiometricPlugin . ENCRYPT _DECRYPT _OPERATION )
// Android docs say we should check if the device is secure before enabling device credential fallback
val manager = getSystemService (
Context . KEYGUARD _SERVICE
@ -54,7 +145,11 @@ class BiometricActivity : AppCompatActivity() {
builder . setTitle ( title ) . setSubtitle ( subtitle ) . setDescription ( description )
if ( Build . VERSION . SDK _INT >= Build . VERSION_CODES . R ) {
var authenticators = BiometricManager . Authenticators . BIOMETRIC _WEAK
var authenticators = if ( cipherOperation ) {
BiometricManager . Authenticators . BIOMETRIC _STRONG
} else {
BiometricManager . Authenticators . BIOMETRIC _WEAK
}
if ( allowDeviceCredential ) {
authenticators = authenticators or BiometricManager . Authenticators . DEVICE _CREDENTIAL
}
@ -76,54 +171,108 @@ class BiometricActivity : AppCompatActivity() {
builder . setConfirmationRequired (
intent . getBooleanExtra ( BiometricPlugin . CONFIRMATION _REQUIRED , true )
)
val promptInfo = builder . build ( )
val prompt = BiometricPrompt (
this ,
executor ,
object : BiometricPrompt . AuthenticationCallback ( ) {
override fun onAuthenticationError (
errorCode : Int ,
errorMessage : CharSequence
) {
super . onAuthenticationError ( errorCode , errorMessage )
finishActivity (
BiometryResultType . ERROR ,
errorCode ,
errorMessage as String
)
}
override fun onAuthenticationSucceeded (
result : BiometricPrompt . AuthenticationResult
) {
super . onAuthenticationSucceeded ( result )
finishActivity ( )
}
return builder . build ( )
}
private fun createBiometricPrompt ( ) : BiometricPrompt {
val executor = if ( Build . VERSION . SDK _INT >= Build . VERSION_CODES . P ) {
this . mainExecutor
} else {
Executor { command : Runnable ? ->
Handler ( this . mainLooper ) . post (
command !!
)
}
)
prompt . authenticate ( promptInfo )
}
val callback = object : BiometricPrompt . AuthenticationCallback ( ) {
override fun onAuthenticationError (
errorCode : Int ,
errorMessage : CharSequence
) {
super . onAuthenticationError ( errorCode , errorMessage )
finishActivity (
BiometryResultType . ERROR ,
errorCode ,
errorMessage as String
)
}
override fun onAuthenticationSucceeded (
result : BiometricPrompt . AuthenticationResult
) {
super . onAuthenticationSucceeded ( result )
authenticationResult = result
finishActivity ( )
}
}
return BiometricPrompt ( this , executor , callback )
}
@JvmOverloads
fun finishActivity (
resultType : BiometryResultType = BiometryResultType . SUCCESS ,
errorCode : Int = 0 ,
errorMessage : String ? = " "
) {
val intent = Intent ( )
val prefix = BiometricPlugin . RESULT _EXTRA _PREFIX
intent
. putExtra ( prefix + BiometricPlugin . RESULT _TYPE , resultType . toString ( ) )
. putExtra ( prefix + BiometricPlugin . RESULT _ERROR _CODE , errorCode )
. putExtra (
prefix + BiometricPlugin . RESULT _ERROR _MESSAGE ,
errorMessage
/ * *
* Get the key if exists , or create a new one if not
* /
private fun getEncryptionKey ( ) : SecretKey {
val keyStore = KeyStore . getInstance ( KEYSTORE _NAME )
keyStore . load ( null )
keyStore . getKey ( KEY _ALIAS , null ) ?. let { return it as SecretKey }
// from here ahead, we will move only if there is not a key with the provided alias
val keyGenerator = KeyGenerator . getInstance ( ENCRYPTION _ALGORITHM , KEYSTORE _NAME )
val builder = KeyGenParameterSpec . Builder (
KEY_ALIAS ,
KeyProperties . PURPOSE _ENCRYPT or KeyProperties . PURPOSE _DECRYPT
)
setResult ( Activity . RESULT _OK , intent )
finish ( )
. setBlockModes ( ENCRYPTION _BLOCK _MODE )
. setEncryptionPaddings ( ENCRYPTION _PADDING )
. setKeySize ( 256 )
. setRandomizedEncryptionRequired ( true )
. setUserAuthenticationRequired ( true ) // Forces to use the biometric authentication to create/retrieve the key
keyGenerator . init ( builder . build ( ) )
return keyGenerator . generateKey ( )
}
private fun getCipher ( ) : Cipher {
val biometricKey = getEncryptionKey ( )
val cipher = Cipher . getInstance ( " $ENCRYPTION _ALGORITHM/ $ENCRYPTION _BLOCK_MODE/ $ENCRYPTION _PADDING " )
if ( cipherType == CipherType . ENCRYPT ) {
cipher . init ( Cipher . ENCRYPT _MODE , biometricKey )
} else {
// Decodes the Base64 string to get the encrypted data and IV
val ( iv , _ ) = decodeEncryptedData ( encryptDecryptData )
cipher . init ( Cipher . DECRYPT _MODE , biometricKey , GCMParameterSpec ( 128 , iv ) )
}
return cipher
}
private fun encodeEncryptedData ( cipher : Cipher , rawEncryptedData : ByteArray ) : String {
val encodedData = Base64 . encodeToString ( rawEncryptedData , Base64 . NO _WRAP )
val encodedIv = Base64 . encodeToString ( cipher . iv , Base64 . NO _WRAP )
val encodedString = encodedData + " ; " + encodedIv
return encodedString
}
private fun decodeEncryptedData ( encryptedDataEncoded : String ) : List < ByteArray > {
val ( data , iv ) = encryptedDataEncoded . split ( " ; " )
val decodedData = Base64 . decode ( data , Base64 . NO _WRAP )
val decodedIv = Base64 . decode ( iv , Base64 . NO _WRAP )
val ret = listOf ( decodedIv , decodedData )
return ret
}
companion object {
var allowDeviceCredential = false
var cipherOperation = false
var encryptDecryptData : String = " "
var authenticationResult : BiometricPrompt . AuthenticationResult ? = null
var cipherType : CipherType = CipherType . ENCRYPT
}
}