MASA MAUI Plugin (五)Android 指纹识别

背景

MAUI的出现,赋予了广大Net开发者开发多平台应用的能力,MAUI 是Xamarin.Forms演变而来,但是相比Xamarin性能更好,可扩展性更强,结构更简单。但是MAUI对于平台相关的实现并不完整。所以MASA团队开展了一个实验性项目,意在对微软MAUI的补充和扩展,项目地址https://github.com/masalabs/MASA.MAUI,每个功能都有单独的demo演示项目,考虑到app安装文件体积(虽然MAUI已经集成裁剪功能,但是该功能对于代码本身有影响),届时每一个功能都会以单独的nuget包的形式提供,方便测试,现在项目才刚刚开始,但是相信很快就会有可以交付的内容啦。

前言

本系列文章面向移动开发小白,从零开始进行平台相关功能开发,演示如何参考平台的官方文档使用MAUI技术来开发相应功能。

介绍

API 级别 23 (Android 6.0) 设备上引入指纹扫描仪为应用程序提供了传统的用户名/密码用户身份验证的替代方法。 相较于用户名和密码,采用指纹对用户进行身份验证使应用程序安全性的实现更具隐私性,之后 API-28(Android9.0) 中添加了生物识别身份验证Biometric,增加了人脸认证相关功能。我们今天讨论的只涉及指纹认证,考虑到兼容性问题采用API - 23 (Android 6.0) 版本提供的 FingerprintManager API,经过测试可以在Android 6.0 -11.0中正常工作,如果您需要人脸验证相关功能请参考链接: androidx.biometric,实现细节与本文类似。

思路

我们先看一下Android 的指纹验证方法核心的指纹管理类FingerprintManagerCompat ,fingerprintManager是通过FingerprintManagerCompat.from(Context context)来创建的。

JAVA代码
FingerprintManagerCompat fingerprintManager= FingerprintManagerCompat.from(Context context);

1、检查资格:

1、需要检查设备是否支持指纹。
2、需要检查设备是否受保护 - 用户必须使用屏幕锁保护设备。 如果用户未使用屏幕锁保护设备,但是当前应用程序对于安全性要求很高,则应通知用户必须配置屏幕锁。
3、需要检查用户是否已经注册指纹 - 用户必须至少有一个指纹已注册到操作系统。 此权限检查应在每次尝试进行身份验证之前进行,因为用户有可能随时取消指纹
在MAUI blazor项目的Platforms->Android文件夹添加MasaMauiFingerprintService.cs 类,添加如下两个方法

public static class MasaMauiFingerprintService
    {
        private static FingerprintManagerCompat fingerprintManager = FingerprintManagerCompat.From(Android.App.Application.Context);

        /// <summary>
        /// Check eligibility
        /// </summary>
        /// <returns>error message</returns>
        public static async Task<string> CheckingEligibility()
        {
            // 1、Check if your hardware supports it
            if (!fingerprintManager.IsHardwareDetected)
            {
                return "IsHardwareDetected";
            }

            // 2、Check if the user is using a screen lock
            // KeyguardManager: Lock screen management class
            var keyguardManager = Android.App.Application.Context.GetSystemService(Context.KeyguardService) as KeyguardManager;
            if (!keyguardManager.IsKeyguardSecure)
            {
                return "The device does not have a screen lock set";
            }

            // 3、Check if at least one fingerprint is registered
            if (!fingerprintManager.HasEnrolledFingerprints)
            {
                return "The device does not have a fingerprint set, please set at least one fingerprint";
            }

            var granted = await CheckAndRequestFingerprintPermission();
            if (!granted)
            {
                return "Permissions not granted";
            }
            return string.Empty;
        }

        /// <summary>
        /// Permission check
        /// </summary>
        /// <returns></returns>
        private static async Task<bool> CheckAndRequestFingerprintPermission()
        {
            var status = await Permissions.CheckStatusAsync<AndroidFingerprintPermissions>();

            if (status == PermissionStatus.Granted)
                return true;

            status = await Permissions.RequestAsync<AndroidFingerprintPermissions>();

            if (status == PermissionStatus.Granted)
                return true;
            return false;
        }

        /// <summary>
        /// Permissions required for fingerprints
        /// </summary>
        private class AndroidFingerprintPermissions : Permissions.BasePlatformPermission
        {
            public override (string androidPermission, bool isRuntime)[] RequiredPermissions =>
                new List<(string androidPermission, bool isRuntime)>
                {
                    (global::Android.Manifest.Permission.UseFingerprint, true),
                }.ToArray();
        }
    }

CheckingEligibility依次检查设备是否支持蓝牙(IsHardwareDetected)、设备是否有屏幕锁(IsKeyguardSecure)这里需要一个KeyguardManager的类帮助检查、是否注册了至少一个指纹(HasEnrolledFingerprints)、是否统一了使用指纹相关权限。

2、扫描指纹实现

现在,我们使用FingerprintManager的Authenticate方法进行指纹验证,

JAVA代码
public void authenticate (FingerprintManager.CryptoObject crypto, 
                CancellationSignal cancel, 
                int flags, 
                FingerprintManager.AuthenticationCallback callback, 
                Handler handler)

参数:
crypto FingerprintManager.CryptoObject: 这是一个加密类的对象,指纹扫描器会使用这个对象来判断认证结果的合法性。这个对象可以是null,但是这样的话,就意味这app无条件信任认证结果,所以这个过程可能被攻击,数据可以被篡改。因此,建议这个参数不要置为null。
cancel CancellationSignal:这个对象用来在指纹识别器扫描用户指纹的是时候取消当前的扫描操作,如果不取消的话,那么指纹扫描器会移植扫描直到超时(一般为30s,取决于具体的厂商实现),这样的话就会比较耗电。建议这个参数不要置为null。识别过程中可以手动取消指纹识别
flags int:没用,传 0
callback FingerprintManager.AuthenticationCallback:要接收身份验证事件的回调方法, 此值不能为 null
handler Handler:FingerprintManagerCompat将会使用这个handler中的looper来处理来自指纹识别硬件的消息。一般来说,我们开发的时候可以直接传null,因为FingerprintManagerCompat会默认使用app的main looper来处理

我们继续在当前目录下添加CryptoObjectHelper.cs

public class CryptoObjectHelper
    {
        // 键值名称,应用中需要保持唯一
        static readonly string KEY_NAME = "com.masa-maui-blazor.android.sample.fingerprint_authentication_key";

        // 写死不用改
        static readonly string KEYSTORE_NAME = "AndroidKeyStore";

        // 加密算法参数 不用改
        static readonly string KEY_ALGORITHM = KeyProperties.KeyAlgorithmAes;
        static readonly string BLOCK_MODE = KeyProperties.BlockModeCbc;
        static readonly string ENCRYPTION_PADDING = KeyProperties.EncryptionPaddingPkcs7;
        static readonly string TRANSFORMATION = KEY_ALGORITHM + "/" +
                                                BLOCK_MODE + "/" +
                                                ENCRYPTION_PADDING;
        readonly KeyStore _keystore;

        public CryptoObjectHelper()
        {
            _keystore = KeyStore.GetInstance(KEYSTORE_NAME);
            _keystore.Load(null);
        }

        public FingerprintManagerCompat.CryptoObject BuildCryptoObject()
        {
            var cipher = CreateCipher();
            return new FingerprintManagerCompat.CryptoObject(cipher);
        }

        Cipher CreateCipher(bool retry = true)
        {
            var key = GetKey();
            var cipher = Cipher.GetInstance(TRANSFORMATION);
            try
            {
                cipher.Init(CipherMode.EncryptMode, key);
            }
            catch (KeyPermanentlyInvalidatedException e)
            {
                _keystore.DeleteEntry(KEY_NAME);
                if (retry)
                {
                    CreateCipher(false);
                }
                else
                {
                    throw new Exception("Could not create the cipher for fingerprint authentication.", e);
                }
            }
            return cipher;
        }

        IKey GetKey()
        {
            IKey secretKey;
            if (!_keystore.IsKeyEntry(KEY_NAME))
            {
                CreateKey();
            }

            secretKey = _keystore.GetKey(KEY_NAME, null);
            return secretKey;
        }

        void CreateKey()
        {
            var keyGen = KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, KEYSTORE_NAME);
            var keyGenSpec =
                new KeyGenParameterSpec.Builder(KEY_NAME, KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt)
                    .SetBlockModes(BLOCK_MODE)
                    .SetEncryptionPaddings(ENCRYPTION_PADDING)
                    .SetUserAuthenticationRequired(true)
                    .Build();
            keyGen.Init(keyGenSpec);
            keyGen.GenerateKey();
        }
    }

CryptoObjectHelper 类使用 Android KeyGenerator 生成密钥并安全地将其存储在设备上。
创建的键类型的元数据由类的 KeyGenParameterSpec 实例提供。
使用GetInstance工厂方法实例化 AKeyGenerator
上述代码使用 (AES) 作为加密算法。 KeyGenParameterSpec.Builde包装具体的AES加密配置信息,
SetUserAuthenticationRequired(true) 表示在使用密钥之前需要用户身份验证。

我们在MasaMauiFingerprintService添加一个验证的方法,其中自定义的MasaMauiAuthCallback,在下面一节介绍。

public static void FingerPrintAuthentication()
        {
            fingerprintManager.Authenticate(new CryptoObjectHelper().BuildCryptoObject(), 0, new CancellationSignal(), new MasaMauiAuthCallback(), null);
        }

3、响应身份验证回调

我们在当前目录继续添加CallBack类MasaMauiAuthCallback.cs ,该类需要继承FingerprintManagerCompat.AuthenticationCallback,至少需要重写OnAuthenticationSucceeded方法

public class MasaMauiAuthCallback : FingerprintManagerCompat.AuthenticationCallback
    {
        // 随便写,但是app内保持唯一
        byte[] SECRET_BYTES = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        
        public override void OnAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result)
        {
            if (result.CryptoObject.Cipher != null) //使用了Cipher
            {
                var doFinalResult = result.CryptoObject.Cipher.DoFinal(SECRET_BYTES);

                if (doFinalResult.Any())
                {
                    MessagingCenter.Send<MasaMauiAuthCallback,string>(this, "Validation", "验证成功");

                }
            }
            else
            {
                // 没有使用Cipher?
                // 我们这里的示例使用了Cipher,暂时不考虑不适用的情况
            }
        }

        public override void OnAuthenticationFailed()
        {
            // 通知用户验证失败
            MessagingCenter.Send<MasaMauiAuthCallback, string>(this, "Validation", "验证失败");
        }
    }

除了OnAuthenticationSucceededOnAuthenticationFailed之外,还有onAuthenticationHelponAuthenticationError
我们这里暂不考虑其他两种情景,有兴趣可以参考链接: AuthenticationCallback 这里为了方便演示,验证成功或者失败都是通过MessagingCenter消息发送出去,在使用的时候需要订阅对应topic,来获取验证结果。

4、测试

新建一个MAUI Blazor项目Masa.Blazor.Maui.Plugin.BiometricsSample
AndroidManifest.xml文件中添加指纹需要的权限 USE_FINGERPRINT

<uses-permission android:name="android.permission.USE_FINGERPRINT" />

简单修改一下Index.razor便于测试
Index.razor:

@page "/"

Welcome to your new app.

<MButton Block OnClick="Fingerprint">验证指纹</MButton>

Index.razor.cs:

using Masa.Blazor.Maui.Plugin.Biometrics;
using Microsoft.AspNetCore.Components;

namespace Masa.Blazor.Maui.Plugin.BiometricsSample.Pages
{
    public partial class Index
    {
        [Inject]
        private IPopupService PopupService { get; set; }
        private async Task Fingerprint()
        {
            var checkingEligibilityErrorMessage = await MasaMauiFingerprintService.CheckingEligibility();
            if (string.IsNullOrEmpty(checkingEligibilityErrorMessage))
            {
                await HandledValidationAsync();
                MasaMauiFingerprintService.FingerPrintAuthentication();
            }
            else
            {
                await PopupService.ToastErrorAsync(checkingEligibilityErrorMessage);
            }
        }

        private async Task HandledValidationAsync()
        {
            // Cancel your subscription first to prevent duplicate subscriptions
            MessagingCenter.Unsubscribe<MasaMauiAuthCallback, string>(this, "Validation");
            MessagingCenter.Subscribe<MasaMauiAuthCallback, string>(this, "Validation", (sender, arg) =>
            {
                PopupService.ToastInfoAsync(arg);
            });
        }
    }
}

这里我使用到MAUI提供的发布和订阅消息MessagingCenter, 参考连接链接: MessagingCenter 代码比较简单,先检查资格,没有报错信息之后开启指纹验证,并异步接收callback方法发布的消息。

启动一下,分别用正确和错误的指纹进行测试:

不同的手机指纹验证的UI不同,我这里是vivo的手机

android获取指纹服务_android