using System; using Android.Content; using Javax.Crypto; using Java.Security; using Java.Lang; using Android.Views.InputMethods; using Android.App; using Android.OS; using Android.Security.Keystore; using Android.Preferences; using Android.Util; using Android.Widget; using AndroidX.Biometric; using AndroidX.Fragment.App; using Java.IO; using Java.Security.Cert; using Java.Util.Concurrent; using Javax.Crypto.Spec; using keepass2android; using Exception = System.Exception; using File = System.IO.File; namespace keepass2android { public interface IBiometricAuthCallback { void OnBiometricAuthSucceeded(); void OnBiometricError(string toString); void OnBiometricAttemptFailed(string message); } public class BiometricModule { public AndroidX.Fragment.App.FragmentActivity Activity { get; set; } public BiometricModule(AndroidX.Fragment.App.FragmentActivity activity) { Activity = activity; } public KeyguardManager KeyguardManager { get { return (KeyguardManager)Activity.GetSystemService("keyguard"); } } public KeyStore Keystore { get { try { return KeyStore.GetInstance("AndroidKeyStore"); } catch (KeyStoreException e) { throw new RuntimeException("Failed to get an instance of KeyStore", e); } } } public KeyGenerator KeyGenerator { get { try { return KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, "AndroidKeyStore"); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Failed to get an instance of KeyGenerator", e); } catch (NoSuchProviderException e) { throw new RuntimeException("Failed to get an instance of KeyGenerator", e); } } } public Cipher Cipher { get { try { return Cipher.GetInstance(KeyProperties.KeyAlgorithmAes + "/" + KeyProperties.BlockModeCbc + "/" + KeyProperties.EncryptionPaddingPkcs7); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Failed to get an instance of Cipher", e); } catch (NoSuchPaddingException e) { throw new RuntimeException("Failed to get an instance of Cipher", e); } } } public ISharedPreferences SharedPreferences { get { return PreferenceManager.GetDefaultSharedPreferences(Activity); } } public bool IsAvailable { get { return BiometricManager.From(Activity).CanAuthenticate() == BiometricManager.BiometricSuccess; } } public bool IsHardwareAvailable { get { var result = BiometricManager.From(Activity).CanAuthenticate(); Kp2aLog.Log("BiometricHardware available = " + result); return result == BiometricManager.BiometricSuccess || result == BiometricManager.BiometricErrorNoneEnrolled; } } } public abstract class BiometricCrypt : IBiometricIdentifier { protected const string FailedToInitCipher = "Failed to init Cipher"; protected readonly string _keyId; protected Cipher _cipher; private CancellationSignal _cancellationSignal; protected BiometricPrompt.CryptoObject _cryptoObject; protected KeyStore _keystore; private BiometricPrompt _biometricPrompt; private FragmentActivity _activity; private BiometricAuthCallbackAdapter _biometricAuthCallbackAdapter; public BiometricCrypt(BiometricModule biometric, string keyId) { Kp2aLog.Log("FP: Create " + this.GetType().Name); _keyId = keyId; _cipher = biometric.Cipher; _keystore = biometric.Keystore; _activity = biometric.Activity; } public abstract bool Init(); protected static string GetAlias(string keyId) { return "keepass2android." + keyId; } protected static string GetIvPrefKey(string prefKey) { return prefKey + "_iv"; } public void StartListening(IBiometricAuthCallback callback) { _biometricAuthCallbackAdapter = new BiometricAuthCallbackAdapter(callback, _activity); StartListening(_biometricAuthCallbackAdapter); } public void StopListening() { Kp2aLog.Log("Fingerprint: StopListening " + (_biometricPrompt != null ? " having prompt " : " without prompt")); _biometricAuthCallbackAdapter?.IgnoreNextError(); _biometricPrompt?.CancelAuthentication(); } public bool HasUserInterface { get { return true; } } public void StartListening(BiometricPrompt.AuthenticationCallback callback) { Kp2aLog.Log("Fingerprint: StartListening "); var executor = Executors.NewSingleThreadExecutor(); _biometricPrompt = new BiometricPrompt(_activity, executor, callback); BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder() .SetTitle(_activity.GetString(AppNames.AppNameResource)) .SetSubtitle(_activity.GetString(Resource.String.unlock_database_title)) .SetNegativeButtonText(_activity.GetString(Android.Resource.String.Cancel)) .SetConfirmationRequired(false) .Build(); _biometricPrompt.Authenticate(promptInfo, _cryptoObject); } public string Encrypt(string textToEncrypt) { Kp2aLog.Log("FP: Encrypting"); return Base64.EncodeToString(_cipher.DoFinal(System.Text.Encoding.UTF8.GetBytes(textToEncrypt)), 0); } public void StoreEncrypted(string textToEncrypt, string prefKey, Context context) { var edit = PreferenceManager.GetDefaultSharedPreferences(context).Edit(); StoreEncrypted(textToEncrypt, prefKey, edit); edit.Commit(); } public void StoreEncrypted(string textToEncrypt, string prefKey, ISharedPreferencesEditor edit) { edit.PutString(prefKey, Encrypt(textToEncrypt)); edit.PutString(GetIvPrefKey(prefKey), Base64.EncodeToString(CipherIv, 0)); } private byte[] CipherIv { get { return _cipher.GetIV(); } } } public interface IBiometricIdentifier { bool Init(); void StartListening(IBiometricAuthCallback callback); void StopListening(); bool HasUserInterface { get; } } public class BiometricDecryption : BiometricCrypt { private readonly Context _context; private readonly byte[] _iv; public BiometricDecryption(BiometricModule biometric, string keyId, byte[] iv) : base(biometric, keyId) { _iv = iv; } public BiometricDecryption(BiometricModule biometric, string keyId, Context context, string prefKey) : base(biometric, keyId) { _context = context; _iv = Base64.Decode(PreferenceManager.GetDefaultSharedPreferences(context).GetString(GetIvPrefKey(prefKey), null), 0); } public static bool IsSetUp(Context context, string prefKey) { return PreferenceManager.GetDefaultSharedPreferences(context).GetString(GetIvPrefKey(prefKey), null) != null; } public override bool Init() { Kp2aLog.Log("FP: Init for Dec"); try { _keystore.Load(null); var aliases = _keystore.Aliases(); if (aliases == null) { Kp2aLog.Log("KS: no aliases"); } else { while (aliases.HasMoreElements) { var o = aliases.NextElement(); Kp2aLog.Log("alias: " + o?.ToString()); } Kp2aLog.Log("KS: end aliases"); } var key = _keystore.GetKey(GetAlias(_keyId), null); if (key == null) throw new Exception("Failed to init cipher for fingerprint Init: key is null"); var ivParams = new IvParameterSpec(_iv); _cipher.Init(CipherMode.DecryptMode, key, ivParams); _cryptoObject = new BiometricPrompt.CryptoObject(_cipher); return true; } catch (KeyPermanentlyInvalidatedException e) { Kp2aLog.Log("FP: KeyPermanentlyInvalidatedException." + e.ToString()); return false; } catch (KeyStoreException e) { throw new RuntimeException(FailedToInitCipher + " (keystore)", e); } catch (CertificateException e) { throw new RuntimeException(FailedToInitCipher + " (CertificateException)", e); } catch (UnrecoverableKeyException e) { throw new RuntimeException(FailedToInitCipher + " (UnrecoverableKeyException)", e); } catch (Java.IO.IOException e) { throw new RuntimeException(FailedToInitCipher + " (IOException)", e); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(FailedToInitCipher + " (NoSuchAlgorithmException)", e); } catch (InvalidKeyException e) { throw new RuntimeException(FailedToInitCipher + " (InvalidKeyException)" + e.ToString(), e); } } public string Decrypt(string encryted) { Kp2aLog.Log("FP: Decrypting "); byte[] encryptedBytes = Base64.Decode(encryted, 0); return System.Text.Encoding.UTF8.GetString(_cipher.DoFinal(encryptedBytes)); } public string DecryptStored(string prefKey) { string enc = PreferenceManager.GetDefaultSharedPreferences(_context).GetString(prefKey, null); return Decrypt(enc); } } public class BiometricEncryption : BiometricCrypt { private KeyGenerator _keyGen; public BiometricEncryption(BiometricModule biometric, string keyId) : base(biometric, keyId) { _keyGen = biometric.KeyGenerator; Kp2aLog.Log("FP: CreateKey "); CreateKey(); } /// /// Creates a symmetric key in the Android Key Store which can only be used after the user /// has authenticated with biometry. /// private void CreateKey() { try { _keystore.Load(null); KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(GetAlias(_keyId), KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt) .SetBlockModes(KeyProperties.BlockModeCbc) // Require the user to authenticate with biometry to authorize every use // of the key .SetEncryptionPaddings(KeyProperties.EncryptionPaddingPkcs7) .SetUserAuthenticationRequired(true); if ((int)Build.VERSION.SdkInt >= 24) builder.SetInvalidatedByBiometricEnrollment(true); _keyGen.Init( builder .Build()); _keyGen.GenerateKey(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } catch (InvalidAlgorithmParameterException e) { throw new RuntimeException(e); } catch (CertificateException e) { throw new RuntimeException(e); } catch (Java.IO.IOException e) { throw new RuntimeException(e); } catch (System.Exception e) { Kp2aLog.LogUnexpectedError(e); } } public override bool Init() { Kp2aLog.Log("FP: Init for Enc "); try { _keystore.Load(null); var key = _keystore.GetKey(GetAlias(_keyId), null); _cipher.Init(CipherMode.EncryptMode, key); _cryptoObject = new BiometricPrompt.CryptoObject(_cipher); return true; } catch (KeyPermanentlyInvalidatedException) { return false; } catch (KeyStoreException e) { throw new RuntimeException(FailedToInitCipher, e); } catch (CertificateException e) { throw new RuntimeException(FailedToInitCipher, e); } catch (UnrecoverableKeyException e) { throw new RuntimeException(FailedToInitCipher, e); } catch (Java.IO.IOException e) { throw new RuntimeException(FailedToInitCipher, e); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(FailedToInitCipher, e); } catch (InvalidKeyException e) { throw new RuntimeException(FailedToInitCipher, e); } } } public class BiometricAuthCallbackAdapter : BiometricPrompt.AuthenticationCallback { private readonly IBiometricAuthCallback _callback; private readonly Context _context; private bool _ignoreNextError; public BiometricAuthCallbackAdapter(IBiometricAuthCallback callback, Context context) { _callback = callback; _context = context; } public override void OnAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) { new Handler(Looper.MainLooper).Post(() => _callback.OnBiometricAuthSucceeded()); } public override void OnAuthenticationError(int errorCode, ICharSequence errString) { if (_ignoreNextError) { _ignoreNextError = false; return; } new Handler(Looper.MainLooper).Post(() => _callback.OnBiometricError(errString.ToString())); } public override void OnAuthenticationFailed() { new Handler(Looper.MainLooper).Post(() => _callback.OnBiometricAttemptFailed(_context.Resources.GetString(Resource.String.fingerprint_not_recognized))); } public void IgnoreNextError() { _ignoreNextError = true; } } }