Files
keepass2android/src/keepass2android-app/BiometricModule.cs
2025-01-07 11:20:08 +01:00

493 lines
16 KiB
C#

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();
}
/// <summary>
/// Creates a symmetric key in the Android Key Store which can only be used after the user
/// has authenticated with biometry.
/// </summary>
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;
}
}
}