安卓应用安全指南 5.7 使用指纹认证功能
5.7 使用指纹认证功能
原书:Android Application Secure Design/Secure Coding Guidebook
译者:飞龙
目前正在研究和开发的各种用于生物认证的方法中,使用面部信息和声音特征的方法尤其突出。在这些方法中,使用指纹认证来识别个体的方法自古以来就有所使用,并且今天被用于签名(通过拇指印)和犯罪调查等目的。指纹识别的应用也在计算机世界的几个领域中得到了发展,并且近年来,这些方法已经开始作为高度便利的技术(提供诸如易于输入的优点)而享有广泛认可,用于一些领域,例如识别智能手机的物主(主要用于解锁屏幕)。
在这些趋势下,Android 6.0(API Level 23)在终端上整合了指纹认证框架,允许应用使用指纹认证功能来识别个人身份。在下面我们将讨论一些使用指纹认证时要记住的安全预防措施。
5.7.1 示例代码
下面我们提供示例代码,来允许应用使用 Android 的指纹认证功能。
要点:
- 声明使用
USE_FINGERPRINT
权限 - 从
AndroidKeyStore
供应器获取实例 - 通知用户需要指纹注册才能创建密钥
- 创建(注册)密钥时,请使用没有漏洞的加密算法(符合标准)
- 创建(注册)密钥时,启用用户(指纹)认证请求(不要指定启用认证的持续时间)
- 设计你的应用的前提是,指纹注册的状态将在密钥创建和使用密钥期间发生变化
- 将加密数据限制为,可通过指纹认证以外的方法恢复(替换)的项东西
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());