安卓应用安全指南 5.7 使用指纹认证功能

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 5.7 使用指纹认证功能 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC BY-NC-SA 4.0目前正在研究和开发的各种用于生物认证的方法中,使用面部信息和声音特征的方法尤其突出。

5.7 使用指纹认证功能

原书:Android Application Secure Design/Secure Coding Guidebook

译者:飞龙

协议:CC BY-NC-SA 4.0

目前正在研究和开发的各种用于生物认证的方法中,使用面部信息和声音特征的方法尤其突出。在这些方法中,使用指纹认证来识别个体的方法自古以来就有所使用,并且今天被用于签名(通过拇指印)和犯罪调查等目的。指纹识别的应用也在计算机世界的几个领域中得到了发展,并且近年来,这些方法已经开始作为高度便利的技术(提供诸如易于输入的优点)而享有广泛认可,用于一些领域,例如识别智能手机的物主(主要用于解锁屏幕)。

在这些趋势下,Android 6.0(API Level 23)在终端上整合了指纹认证框架,允许应用使用指纹认证功能来识别个人身份。在下面我们将讨论一些使用指纹认证时要记住的安全预防措施。

5.7.1 示例代码

下面我们提供示例代码,来允许应用使用 Android 的指纹认证功能。

要点:

  1. 声明使用USE_FINGERPRINT权限
  2. AndroidKeyStore供应器获取实例
  3. 通知用户需要指纹注册才能创建密钥
  4. 创建(注册)密钥时,请使用没有漏洞的加密算法(符合标准)
  5. 创建(注册)密钥时,启用用户(指纹)认证请求(不要指定启用认证的持续时间)
  6. 设计你的应用的前提是,指纹注册的状态将在密钥创建和使用密钥期间发生变化
  7. 将加密数据限制为,可通过指纹认证以外的方法恢复(替换)的项东西

MainActivity.java

package authentication.fingerprint.android.jssec.org.fingerprintauthentication;

import android.app.AlertDialog;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Base64;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;

public class MainActivity extends AppCompatActivity {

    private FingerprintAuthentication mFingerprintAuthentication;
    private static final String SENSITIVE_DATA = "sensitive data";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mFingerprintAuthentication = new FingerprintAuthentication(this);
        Button button_fingerprint_auth = (Button) findViewById(R.id.button_fingerprint_auth);
        button_fingerprint_auth.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                if (!mFingerprintAuthentication.isAuthenticating()) {
                    if (authenticateByFingerprint()) {
                        showEncryptedData(null);
                        setAuthenticationState(true);
                    }
                } else {
                    mFingerprintAuthentication.cancel();
                }
            }
        });
    }

    private boolean authenticateByFingerprint() {
        if (!mFingerprintAuthentication.isFingerprintHardwareDetected()) {
            // Terminal is not equipped with a fingerprint sensor
            return false;
        }
        if (!mFingerprintAuthentication.isFingerprintAuthAvailable()) {
            // *** POINT 3 *** Notify users that fingerprint registration will be required to create a key
            new AlertDialog.Builder(this)
                .setTitle(R.string.app_name)
                .setMessage("No fingerprint information has been registered.¥n" +
                    "Click ¥"Security¥" on the Settings menu to register fingerprints. ¥n" +
                    "Registering fingerprints allows easy authentication.")
                .setPositiveButton("OK", null)
                .show();
            return false;
        }
        // Callback that receives the results of fingerprint authentication
        FingerprintManager.AuthenticationCallback callback = new FingerprintManager.AuthenticationCallback() {

            @Override
            public void onAuthenticationError(int errorCode, CharSequence errString) {
                showMessage(errString, R.color.colorError);
                reset();
            }

            @Override
            public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
                showMessage(helpString, R.color.colorHelp);
            }

            @Override
            public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
                Cipher cipher = result.getCryptoObject().getCipher();
                try {
                    // *** POINT 7*** Restrict encrypted data to items that can be restored (replaced) by methods other than fingerprint authentication
                    byte[] encrypted = cipher.doFinal(SENSITIVE_DATA.getBytes());
                    showEncryptedData(encrypted);
                } catch (IllegalBlockSizeException | BadPaddingException e) {
                }
                showMessage(getString(R.string.fingerprint_auth_succeeded), R.color.colorAuthenticated);
                reset();
            }
            @Override
            public void onAuthenticationFailed() {
                showMessage(getString(R.string.fingerprint_auth_failed), R.color.colorError);
            }
        };
        if (mFingerprintAuthentication.startAuthentication(callback)) {
            showMessage(getString(R.string.fingerprint_processing), R.color.colorNormal);
            return true;
        }
        return false;
    }

    private void setAuthenticationState(boolean authenticating) {
        Button button = (Button) findViewById(R.id.button_fingerprint_auth);
        button.setText(authenticating ? R.string.cancel : R.string.authenticate);
    }

    private void showEncryptedData(byte[] encrypted) {
        TextView textView = (TextView) findViewById(R.id.encryptedData);
        if (encrypted != null) {
            textView.setText(Base64.encodeToString(encrypted, 0));
        } else {
            textView.setText("");
        }
    }

    private String getCurrentTimeString() {
        long currentTimeMillis = System.currentTimeMillis();
        Date date = new Date(currentTimeMillis);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss.SSS");
        return simpleDateFormat.format(date);
    }

    private void showMessage(CharSequence msg, int colorId) {
        TextView textView = (TextView) findViewById(R.id.textView);
        textView.setText(getCurrentTimeString() + " :¥n" + msg);
        textView.setTextColor(getResources().getColor(colorId, null));
    }

    private void reset() {
        setAuthenticationState(false);
    }
}

FingerprintAuthentication.java

package authentication.fingerprint.android.jssec.org.fingerprintauthentication;

import android.app.KeyguardManager;
import android.content.Context;
import android.hardware.fingerprint.FingerprintManager;
import android.os.CancellationSignal;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyInfo;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;

public class FingerprintAuthentication {

    private static final String KEY_NAME = "KeyForFingerprintAuthentication";
    private static final String PROVIDER_NAME = "AndroidKeyStore";
    private KeyguardManager mKeyguardManager;
    private FingerprintManager mFingerprintManager;
    private CancellationSignal mCancellationSignal;
    private KeyStore mKeyStore;
    private KeyGenerator mKeyGenerator;
    private Cipher mCipher;

    public FingerprintAuthentication(Context context) {
        mKeyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
        mFingerprintManager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE
        );
        reset();
    }

    public boolean startAuthentication(final FingerprintManager.AuthenticationCallback callback) {
        if (!generateAndStoreKey())
            return false;
        if (!initializeCipherObject())
            return false;
        FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(mCipher);
        mCancellationSignal = new CancellationSignal();
        // Callback to receive the results of fingerprint authentication
        FingerprintManager.AuthenticationCallback hook = new FingerprintManager.AuthenticationCallback() {

            @Override
            public void onAuthenticationError(int errorCode, CharSequence errString) {
                if (callback != null) 
                    callback.onAuthenticationError(errorCode, errString);
                reset();
            }

            @Override
            public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
                if (callback != null) 
                    callback.onAuthenticationHelp(helpCode, helpString);
            }

            @Override
            public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
                if (callback != null) 
                    callback.onAuthenticationSucceeded(result);
                reset();
            }

            @Override
            public void onAuthenticationFailed() {
                if (callback != null) 
                    callback.onAuthenticationFailed();
            }
        };
        // Execute fingerprint authentication
        mFingerprintManager.authenticate(cryptoObject, mCancellationSignal, 0, hook, null);
        return true;
    }

    public boolean isAuthenticating() {
        return mCancellationSignal != null && !mCancellationSignal.isCanceled();
    }

    public void cancel() {
        if (mCancellationSignal != null) {
            if (!mCancellationSignal.isCanceled())

                mCancellationSignal.cancel();
        }
    }

    private void reset() {
        try {
            // *** POINT 2 *** Obtain an instance from the "AndroidKeyStore" Provider
            mKeyStore = KeyStore.getInstance(PROVIDER_NAME);
            mKeyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, PROVIDER_NAME);
            mCipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES
                + "/" + KeyProperties.BLOCK_MODE_CBC
                + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7);
        } catch (KeyStoreException | NoSuchPaddingException
            | NoSuchAlgorithmException | NoSuchProviderException e) {
            throw new RuntimeException("failed to get cipher instances", e);
        }
        mCancellationSignal = null;
    }

    public boolean isFingerprintAuthAvailable() {
        return (mKeyguardManager.isKeyguardSecure()
            && mFingerprintManager.hasEnrolledFingerprints()) ? true : false;
    }

    public boolean isFingerprintHardwareDetected() {
        return mFingerprintManager.isHardwareDetected();
    }

    private boolean generateAndStoreKey() {
        try {
            mKeyStore.load(null);
            if (mKeyStore.containsAlias(KEY_NAME))
                mKeyStore.deleteEntry(KEY_NAME);
            mKeyGenerator.init(
                // *** POINT 4 *** When creating (registering) keys, use an encryption algorithm that is not vulnerable (meets standards)
                new KeyGenParameterSpec.Builder(KEY_NAME, KeyProperties.PURPOSE_ENCRYPT)
                    .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
                // *** POINT 5 *** When creating (registering) keys, enable requests for user (fingerprint) authentication (do not specify the duration over which authentication is enabled)
                    .setUserAuthenticationRequired(true)
                    .build());
            // Generate a key and store it in Keystore(AndroidKeyStore)
            mKeyGenerator.generateKey();
            return true;
        } catch (IllegalStateException e) {
            return false;
        } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
            | CertificateException | KeyStoreException | IOException e) {
            throw new RuntimeException("failed to generate a key", e);
        }
    }

    private boolean initializeCipherObject() {
        try {
            mKeyStore.load(null);
            SecretKey key = (SecretKey) mKeyStore.getKey(KEY_NAME, null);
            SecretKeyFactory factory = SecretKeyFactory.getInstance(KeyProperties.KEY_ALGORITHM_AES, PROVIDER_NAME);
            KeyInfo info = (KeyInfo) factory.getKeySpec(key, KeyInfo.class);
            mCipher.init(Cipher.ENCRYPT_MODE, key);
            return true;
        } catch (KeyPermanentlyInvalidatedException e) {
            // *** POINT 6 *** Design your app on the assumption that the status of fingerprint registration will change between when keys are created and when keys are used
            return false;
        } catch (KeyStoreException | CertificateException 
            | UnrecoverableKeyException | IOException
            | NoSuchAlgorithmException | InvalidKeySpecException 
            | NoSuchProviderException | InvalidKeyException e) {
            throw new RuntimeException("failed to init Cipher", e);
        }
    }
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="authentication.fingerprint.android.jssec.org.fingerprintauthentication" >
    <!-- +++ POINT 1 *** Declare the use of the USE_FINGERPRINT permission -->
    <uses-permission android:name="android.permission.USE_FINGERPRINT" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:screenOrientation="portrait" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

5.7.2 规则书

使用指纹认证的时候,遵循下列规则:

5.7.2.1 创建(注册)密钥时,请使用没有漏洞的加密算法(符合标准)(必需)

与“5.6 使用密码学”中讨论的密码密钥和公密一样,使用指纹认证功能来创建密钥时,必须使用没有漏洞的加密算法 - 即符合某些标准的算法,来防止第三方的窃听。 事实上,安全和没有漏洞的选择不仅适用于加密算法,而且适用于加密模式和填充。

算法选择的更多信息,请参见“5.6.2.2 使用强算法(特别是符合相关标准的算法)(必需)”部分。

5.7.2.2 将加密数据限制为,可通过指纹认证以外的方法恢复(替换)的东西(必需)

当应用使用指纹认证功能,对应用中的数据进行加密时,应用的设计必须允许通过指纹认证以外的方法恢复(替换)数据。 一般来说,使用生物信息会带来各种问题 - 包括保密性,修改难度和错误识别 - 因此,最好避免单纯依靠生物信息进行认证。

例如,假设应用内部的数据使用密钥加密,密钥由指纹认证功能生成,但存储在终端内的指纹数据随后会被用户删除。 然后用于加密数据的密钥不可用,也不可能复制数据。 如果数据不能通过指纹认证功能以外的某种方式恢复,则存在数据无法使用的巨大风险。

此外,指纹信息的删除不是唯一的情况,即使用指纹认证功能创建的密钥可能变得不可用。 在 Nexus5X 中,如果使用指纹认证功能来创建密钥,然后将该密钥注册为额外的指纹信息,则据观察,之前创建的密钥不可用 [30]。此外,不能排除这种可能性,由于指纹传感器的错误识别,通常可以正确使用的密钥变得不可用。

[30] 信息来自 2016 年 9 月 1 日的版本。 这可能会在未来进行修改。

5.7.2.3 通知用户需要注册指纹才能创建密钥(推荐)

为了使用指纹认证创建密钥,有必要在终端上注册用户的指纹。 设计应用来引导用户进入设置菜单来鼓励指纹注册时,开发人员必须记住,指纹代表重要的个人数据,并且希望向用户解释为什么应用使用指纹信息是必要的或便利的。

通知用户需要注册指纹

if (!mFingerprintAuthentication.isFingerprintAuthAvailable()) {
    // **Point** Notify users that fingerprint registration will be required to create a key
    new AlertDialog.Builder(this)
        .setTitle(R.string.app_name)
        .setMessage("No fingerprint information has been registered.¥n" +
            " Click ¥"Security¥" on the Settings menu to register fingerprints.¥n" +
            " Registering fingerprints allows easy authentication.")
        .setPositiveButton("OK", null)
        .show();
    return false;
}

5.7.3 高级话题

5.7.3.1 Android 应用使用指纹认证功能的先决条件

为了让应用使用指纹认证,必须满足以下两个条件。

  • 用户指纹必须在终端内注册。
  • (特定于应用的)密钥必须关联注册的指纹。

注册用户指纹

用户指纹信息只能通过设置菜单中的“安全”选项进行注册;一般应用不能执行指纹注册过程。 因此,如果应用尝试使用指纹认证功能时未注册指纹,则应用必须引导用户进入设置菜单并鼓励用户注册指纹。 此时,应用需要向用户提供一些解释,说明为什么使用指纹信息是必要和方便的。

另外,作为指纹注册的必要前提条件,终端必须配置一个替代的屏幕锁定机制。 在指纹已在终端中注册的状态下,如果屏幕锁定被禁用,注册的指纹信息将被删除。

创建和注册密钥

为了关联密钥和终端中注册的指纹,请使用由AndroidKeyStore供应器提供的KeyStore实例,来创建并注册新密钥或注册现有密钥。 为了创建关联指纹信息的密钥,请在创建KeyGenerator时配置参数设置,来启用用户认证请求。

创建并注册关联指纹信息的密钥

try {
    // Obtain an instance from the "AndroidKeyStore" Provider
    KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
    keyGenerator.init(
        new KeyGenParameterSpec.Builder(KEY_NAME, KeyProperties.PURPOSE_ENCRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
            .setUserAuthenticationRequired(true) // Enable requests for user (fingerprint) authentication
            .build());
    keyGenerator.generateKey();
} catch (IllegalStateException e) {
    // no fingerprints have been registered in this terminal
    throw new RuntimeException(“No fingerprint registered”, e);
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
    CertificateException | KeyStoreException | IOException e) {
    // failed to generate a key
    throw new RuntimeException("Failed to generate a key", e);
}

为了关联指纹信息和现有密钥,请使用KeyStore条目,将该密钥注册到已添加设置的东西,来启用用户认证请求。

关联指纹信息和现有密钥

SecretKey key = …; // existing key
KeyStore keyStore = KeyStore.getInstance(“AndroidKeyStore”);
keyStore.load(null);
keyStore.setEntry(
    "alias_for_the_key",
    new KeyStore.SecretKeyEntry(key),
    new KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT)
        .setUserAuthenticationRequired(true) // Enable requests for user (fingerprint) authentication
    .build());
相关文章
|
1月前
|
存储 安全 Android开发
探索Android系统的最新安全特性
在数字时代,智能手机已成为我们生活中不可或缺的一部分。随着技术的不断进步,手机操作系统的安全性也越来越受到重视。本文将深入探讨Android系统最新的安全特性,包括其设计理念、实施方式以及对用户的影响。通过分析这些安全措施如何保护用户免受恶意软件和网络攻击的威胁,我们希望为读者提供对Android安全性的全面了解。
|
3月前
|
Android开发
Android开发表情emoji功能开发
本文介绍了一种在Android应用中实现emoji表情功能的方法,通过将图片与表情字符对应,实现在`TextView`中的正常显示。示例代码展示了如何使用自定义适配器加载emoji表情,并在编辑框中输入或删除表情。项目包含完整的源码结构,可作为开发参考。视频演示和源码详情见文章内链接。
84 4
Android开发表情emoji功能开发
|
3月前
|
安全 Android开发 iOS开发
Android vs iOS:探索移动操作系统的设计与功能差异###
【10月更文挑战第20天】 本文深入分析了Android和iOS两个主流移动操作系统在设计哲学、用户体验、技术架构等方面的显著差异。通过对比,揭示了这两种系统各自的独特优势与局限性,并探讨了它们如何塑造了我们的数字生活方式。无论你是开发者还是普通用户,理解这些差异都有助于更好地选择和使用你的移动设备。 ###
66 3
|
5月前
|
编解码 测试技术 Android开发
Android经典实战之用 CameraX 库实现高质量的照片和视频拍摄功能
本文详细介绍了如何利用CameraX库实现高质量的照片及视频拍摄功能,包括添加依赖、初始化、权限请求、配置预览与捕获等关键步骤。此外,还特别针对不同分辨率和帧率的视频拍摄提供了性能优化策略,确保应用既高效又稳定。
497 1
Android经典实战之用 CameraX 库实现高质量的照片和视频拍摄功能
|
3月前
|
安全 网络安全 Android开发
深度解析:利用Universal Links与Android App Links实现无缝网页至应用跳转的安全考量
【10月更文挑战第2天】在移动互联网时代,用户经常需要从网页无缝跳转到移动应用中。这种跳转不仅需要提供流畅的用户体验,还要确保安全性。本文将深入探讨如何利用Universal Links(仅限于iOS)和Android App Links技术实现这一目标,并分析其安全性。
453 0
|
4月前
|
Android开发 开发者
Android平台无纸化同屏如何实现实时录像功能
Android平台无纸化同屏,如果需要本地录像的话,实现难度不大,只要复用之前开发的录像模块的就可以,对我们来说,同屏采集这块,只是数据源不同而已,如果是自采集的其他数据,我们一样可以编码录像。
|
5月前
|
图形学 Android开发
小功能⭐️Unity调用Android常用事件
小功能⭐️Unity调用Android常用事件
|
6月前
|
存储 安全 数据安全/隐私保护
🔎Android安全攻防实战!守护你的应用数据安全,让用户放心使用!🛡️
【7月更文挑战第28天】在移动应用盛行的时代,确保Android应用安全性至关重要。本文以问答形式探讨了主要安全威胁(如逆向工程、数据窃取)及其对策。建议使用代码混淆、签名验证、数据加密等技术来增强应用保护。此外,还推荐了加密API、HTTPS通信、代码审计等措施来进一步加强安全性。综上所述,全面的安全策略对于构建安全可靠的应用环境必不可少。#Android #应用安全 #代码混淆 #数据加密
108 3
|
6月前
|
存储 安全 Android开发
安卓应用开发的安全之道
【7月更文挑战第4天】在数字时代,移动应用的安全性至关重要。本文将深入探讨在安卓平台上开发安全应用的最佳实践,包括代码混淆、数据存储加密、网络通信安全、权限管理以及定期的安全审计和更新策略。通过这些措施,开发者可以显著提高他们的应用抵御恶意攻击的能力,保护用户数据免受侵害。
|
7月前
|
数据库 Android开发 数据安全/隐私保护
在 Android Studio 中结合使用 SQLite 数据库实现简单的注册和登录功能
在 Android Studio 中结合使用 SQLite 数据库实现简单的注册和登录功能
299 2