493 lines
16 KiB
C#
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;
|
|
}
|
|
}
|
|
|
|
} |