跳过内容

Android 本地身份验证

概述

在本地身份验证期间,应用程序根据存储在设备本地的凭据对用户进行身份验证。换句话说,用户通过提供有效的 PIN 码、密码或生物特征(例如面部或指纹)来“解锁”应用程序或某些内部功能层,这些凭据通过引用本地数据进行验证。通常,这样做是为了让用户更方便地恢复与远程服务的现有会话,或者作为一种逐步身份验证的方式来保护某些关键功能。

正如在“移动应用身份验证架构”一章中所述:测试人员应该意识到,本地身份验证应始终在远程端点强制执行,或者基于加密原语。如果没有从身份验证过程中返回任何数据,攻击者可以很容易地绕过本地身份验证。

在 Android 上,Android 运行时支持两种本地身份验证机制:确认凭据流程和生物识别身份验证流程。

确认凭据流程

自 Android 6.0 起提供确认凭据流程,用于确保用户不必输入特定于应用程序的密码以及锁定屏幕保护。相反:如果用户最近已登录到设备,则可以使用确认凭据从AndroidKeystore解锁加密材料。也就是说,如果用户在设定的时限内(setUserAuthenticationValidityDurationSeconds)解锁了设备,否则需要再次解锁设备。

请注意,确认凭据的安全性仅与锁定屏幕上设置的保护措施一样强大。这通常意味着使用简单的预测性锁定屏幕模式,因此我们不建议任何需要 L2 安全控制的应用程序使用确认凭据。

生物识别身份验证流程

生物识别身份验证是一种方便的身份验证机制,但在使用时也会引入额外的攻击面。Android 开发者文档提供了一个有趣的概述和指标,用于衡量生物识别解锁安全性

Android 平台提供三种不同的生物识别身份验证类

  • Android 10(API 级别 29)及更高版本:BiometricManager
  • Android 9(API 级别 28)及更高版本:BiometricPrompt
  • Android 6.0(API 级别 23)及更高版本:FingerprintManager(在 Android 9 (API level 28) 中已弃用)

BiometricManager可用于验证设备上是否有可用的生物识别硬件以及是否已由用户配置。如果是这种情况,则可以使用类BiometricPrompt显示系统提供的生物识别对话框。

BiometricPrompt类是一个重大改进,因为它允许在 Android 上为生物识别身份验证提供一致的 UI,并且还支持比指纹更多的传感器。

这与FingerprintManager类不同,后者仅支持指纹传感器,不提供 UI,迫使开发人员构建自己的指纹 UI。

关于 Android 上的生物识别 API 的非常详细的概述和说明已在Android 开发者博客上发布。

FingerprintManager(在 Android 9 (API level 28) 中已弃用)

Android 6.0(API 级别 23)引入了通过指纹验证用户的公共 API,但在 Android 9(API 级别 28)中已弃用。通过FingerprintManager类提供对指纹硬件的访问权限。应用程序可以通过实例化FingerprintManager对象并调用其authenticate方法来请求指纹身份验证。调用者注册回调方法来处理身份验证过程的可能结果(即成功、失败或错误)。请注意,此方法并不构成指纹身份验证已实际执行的有力证明 - 例如,身份验证步骤可能会被攻击者修补掉,或者可以使用动态检测来重载“成功”回调。

通过将指纹 API 与 Android KeyGenerator类结合使用,可以实现更好的安全性。通过这种方法,对称密钥存储在 Android KeyStore 中,并使用用户的指纹解锁。例如,为了使用户能够访问远程服务,创建一个 AES 密钥来加密身份验证令牌。通过在创建密钥时调用setUserAuthenticationRequired(true),可以确保用户必须重新进行身份验证才能检索它。然后可以将加密的身份验证令牌直接保存在设备上(例如,通过共享首选项)。这种设计是一种相对安全的方式,可以确保用户实际输入了授权的指纹。

更安全的选择是使用非对称加密。在这里,移动应用程序在 KeyStore 中创建一个非对称密钥对,并在服务器后端注册公钥。然后,以后的事务使用私钥签名,并由服务器使用公钥进行验证。

生物识别库

Android 提供了一个名为Biometric的库,它提供BiometricPromptBiometricManager API 的兼容性版本,正如在 Android 10 中实现的那样,完全支持 Android 6.0 (API 23)。

您可以在 Android 开发者文档中找到参考实现以及有关如何显示生物识别身份验证对话框的说明。

BiometricPrompt类中有两种authenticate方法可用。其中一种方法需要一个CryptoObject,这为生物识别身份验证添加了额外的安全层。

使用 CryptoObject 时,身份验证流程如下

  • 该应用程序在 KeyStore 中创建一个密钥,并将setUserAuthenticationRequiredsetInvalidatedByBiometricEnrollment设置为 true。此外,setUserAuthenticationValidityDurationSeconds应设置为 -1。
  • 此密钥用于加密用于验证用户身份的信息(例如,会话信息或身份验证令牌)。
  • 在密钥从 KeyStore 释放以解密数据之前,必须提供一组有效的生物识别特征,该数据通过authenticate方法和CryptoObject进行验证。
  • 即使在已 root 的设备上,此解决方案也无法绕过,因为 KeyStore 中的密钥只能在成功的生物识别身份验证后才能使用。

如果CryptoObject未用作身份验证方法的一部分,则可以使用 Frida 绕过它。有关更多详细信息,请参阅“动态检测”部分。

开发人员可以使用 Android 提供的几个验证类来测试其应用程序中生物识别身份验证的实现。

FingerprintManager

本节介绍如何使用FingerprintManager类实现生物识别身份验证。请记住,此类已弃用,作为最佳实践,应使用生物识别库。本节仅供参考,以防您遇到此类实现并需要对其进行分析。

首先搜索FingerprintManager.authenticate调用。传递给此方法的第一个参数应该是CryptoObject实例,它是FingerprintManager 支持的加密对象的包装类。如果该参数设置为null,则意味着指纹授权纯粹是事件绑定的,可能会产生安全问题。

用于初始化密码包装器的密钥的创建可以追溯到CryptoObject。验证密钥是否都使用KeyGenerator类创建,以及在创建KeyGenParameterSpec对象期间是否调用了setUserAuthenticationRequired(true)(参见下面的代码示例)。

请务必验证身份验证逻辑。为了使身份验证成功,远程端点必须要求客户端提供从 KeyStore 检索的密钥、从密钥派生的值或使用客户端私钥签名的值(参见上文)。

安全地实现指纹身份验证需要遵循一些简单的原则,首先检查是否可以使用该类型的身份验证。最基本的是,设备必须运行 Android 6.0 或更高版本 (API 23+)。还需要验证其他四个先决条件

  • 必须在 Android 清单中请求权限

    <uses-permission
        android:name="android.permission.USE_FINGERPRINT" />
    
  • 指纹硬件必须可用

    FingerprintManager fingerprintManager = (FingerprintManager)
                    context.getSystemService(Context.FINGERPRINT_SERVICE);
    fingerprintManager.isHardwareDetected();
    
  • 用户必须拥有受保护的锁定屏幕

    KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
    keyguardManager.isKeyguardSecure();  //note if this is not the case: ask the user to setup a protected lock screen
    
  • 至少应注册一根手指

    fingerprintManager.hasEnrolledFingerprints();
    
  • 应用程序应具有请求用户指纹的权限

    context.checkSelfPermission(Manifest.permission.USE_FINGERPRINT) == PermissionResult.PERMISSION_GRANTED;
    

如果以上任何检查失败,则不应提供指纹身份验证选项。

重要的是要记住,并非每个 Android 设备都提供硬件支持的密钥存储。KeyInfo类可用于确定密钥是否驻留在安全硬件(例如可信执行环境 (TEE) 或安全元件 (SE))内。

SecretKeyFactory factory = SecretKeyFactory.getInstance(getEncryptionKey().getAlgorithm(), ANDROID_KEYSTORE);
KeyInfo secetkeyInfo = (KeyInfo) factory.getKeySpec(yourencryptionkeyhere, KeyInfo.class);
secetkeyInfo.isInsideSecureHardware()

在某些系统中,也可以通过硬件强制执行生物识别身份验证策略。这通过以下方式进行检查

keyInfo.isUserAuthenticationRequirementEnforcedBySecureHardware();

以下描述了如何使用对称密钥对进行指纹身份验证。

可以通过使用KeyGenerator类创建新的 AES 密钥,并在KeyGenParameterSpec.Builder中添加setUserAuthenticationRequired(true)来实现指纹身份验证。

generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE);

generator.init(new KeyGenParameterSpec.Builder (KEY_ALIAS,
        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
        .setUserAuthenticationRequired(true)
        .build()
);

generator.generateKey();

要使用受保护的密钥执行加密或解密,请创建一个Cipher对象,并使用密钥别名对其进行初始化。

SecretKey keyspec = (SecretKey)keyStore.getKey(KEY_ALIAS, null);

if (mode == Cipher.ENCRYPT_MODE) {
    cipher.init(mode, keyspec);

请记住,新密钥不能立即使用 - 必须首先通过FingerprintManager对其进行身份验证。这涉及将Cipher对象包装到FingerprintManager.CryptoObject中,该对象在被识别之前传递给FingerprintManager.authenticate

cryptoObject = new FingerprintManager.CryptoObject(cipher);
fingerprintManager.authenticate(cryptoObject, new CancellationSignal(), 0, this, null);

当身份验证成功时,将调用回调方法onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result)。然后可以从结果中检索经过身份验证的CryptoObject

public void authenticationSucceeded(FingerprintManager.AuthenticationResult result) {
    cipher = result.getCryptoObject().getCipher();

    //(... do something with the authenticated cipher object ...)
}

以下描述了如何使用非对称密钥对进行指纹身份验证。

要使用非对称加密实现指纹身份验证,首先使用KeyPairGenerator类创建一个签名密钥,并在服务器上注册公钥。然后,您可以通过在客户端上对数据进行签名并在服务器上验证签名来验证数据片段。有关使用指纹 API 向远程服务器进行身份验证的详细示例,请参见Android Developers Blog

密钥对按如下方式生成

KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
keyPairGenerator.initialize(
        new KeyGenParameterSpec.Builder(MY_KEY,
                KeyProperties.PURPOSE_SIGN)
                .setDigests(KeyProperties.DIGEST_SHA256)
                .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1"))
                .setUserAuthenticationRequired(true)
                .build());
keyPairGenerator.generateKeyPair();

要使用密钥进行签名,您需要实例化一个 CryptoObject 并通过FingerprintManager对其进行身份验证。

Signature.getInstance("SHA256withECDSA");
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
PrivateKey key = (PrivateKey) keyStore.getKey(MY_KEY, null);
signature.initSign(key);
CryptoObject cryptoObject = new FingerprintManager.CryptoObject(signature);

CancellationSignal cancellationSignal = new CancellationSignal();
FingerprintManager fingerprintManager =
        context.getSystemService(FingerprintManager.class);
fingerprintManager.authenticate(cryptoObject, cancellationSignal, 0, this, null);

现在您可以按如下方式签署字节数组inputBytes的内容。

Signature signature = cryptoObject.getSignature();
signature.update(inputBytes);
byte[] signed = signature.sign();
  • 请注意,在签名交易的情况下,应生成一个随机数并将其添加到签名数据中。否则,攻击者可能会重放交易。
  • 要使用对称指纹身份验证来实现身份验证,请使用质询-响应协议。

其他安全功能

Android 7.0(API 级别 24)将setInvalidatedByBiometricEnrollment(boolean invalidateKey)方法添加到KeyGenParameterSpec.Builder。当invalidateKey值设置为true(默认值)时,当注册新指纹时,对指纹身份验证有效的密钥将被不可逆转地失效。这可以防止攻击者检索密钥,即使他们能够注册额外的指纹也是如此。

Android 8.0(API 级别 26)添加了两个额外的错误代码

  • FINGERPRINT_ERROR_LOCKOUT_PERMANENT:用户尝试使用指纹读取器解锁设备的次数过多。
  • FINGERPRINT_ERROR_VENDOR:发生了特定于供应商的指纹读取器错误。

实施生物识别身份验证

确保设置了锁定屏幕

KeyguardManager mKeyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
if (!mKeyguardManager.isKeyguardSecure()) {
    // Show a message that the user hasn't set up a lock screen.
}
  • 创建受锁定屏幕保护的密钥。为了使用此密钥,用户需要在过去的 X 秒内解锁设备,或者需要再次解锁设备。确保此超时时间不太长,因为它变得难以确保使用该应用程序的用户与解锁设备的用户是同一人

    try {
        KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
        keyStore.load(null);
        KeyGenerator keyGenerator = KeyGenerator.getInstance(
                KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
    
        // Set the alias of the entry in Android KeyStore where the key will appear
        // and the constrains (purposes) in the constructor of the Builder
        keyGenerator.init(new KeyGenParameterSpec.Builder(KEY_NAME,
                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                .setUserAuthenticationRequired(true)
                        // Require that the user has unlocked in the last 30 seconds
                .setUserAuthenticationValidityDurationSeconds(30)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
                .build());
        keyGenerator.generateKey();
    } catch (NoSuchAlgorithmException | NoSuchProviderException
            | InvalidAlgorithmParameterException | KeyStoreException
            | CertificateException | IOException e) {
        throw new RuntimeException("Failed to create a symmetric key", e);
    }
    
  • 设置锁定屏幕以进行确认

    private static final int REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS = 1; //used as a number to verify whether this is where the activity results from
    Intent intent = mKeyguardManager.createConfirmDeviceCredentialIntent(null, null);
    if (intent != null) {
        startActivityForResult(intent, REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS);
    }
    
  • 锁定屏幕后使用密钥

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS) {
            // Challenge completed, proceed with using cipher
            if (resultCode == RESULT_OK) {
                //use the key for the actual authentication flow
            } else {
                // The user canceled or didn’t complete the lock screen
                // operation. Go to error/cancellation flow.
            }
        }
    }
    

第三方 SDK

确保指纹身份验证和/或其他类型的生物识别身份验证完全基于 Android SDK 及其 API。如果不是这种情况,请确保已对替代 SDK 进行了适当的审查以查找任何弱点。确保 SDK 由 TEE/SE 支持,后者根据生物识别身份验证解锁(加密)密钥。此密钥不应被其他任何东西解锁,而应被有效的生物识别条目解锁。这样,指纹逻辑永远不应被绕过。