在 FingerprintManager.authenticate 期间,对于以下用例,我是否需要 CryptoObject 对象或 null

Do I need CryptoObject object, or null for the following use case during FingerprintManager.authenticate

当我们打电话时

mFingerprintManager
            .authenticate(cryptoObject, 0 /* flags */, mCancellationSignal, this, null);

我注意到 cryptoObject 通过 null 完全没问题。根据FingerprintManager documentation

FingerprintManager.CryptoObject: object associated with the call or null if none required.


根据 https://github.com/googlesamples/android-FingerprintDialog,它显示创建 CryptoObject 的冗长步骤。


所以,我不确定我的用例是应该使用 CryptoObject 还是 null。我已经阅读 Why crypto object is needed for Android fingerprint authentication? 但仍然无法完全理解并为我的情况做出决定。

我的用例如下。

我有一个笔记应用程序的启动锁定屏幕。通常,用户在启用开机锁屏时,需要设置花样绘制。万一他忘记了他的图案绘制,他可以使用他的指纹作为替代。该应用程序如下所示


这是源代码。目前,我正在使用 CryptoObject。但是,根据我的用户反馈,他们中的少数人在使用此新功能时遇到了一些应用程序问题。虽然我们在 Google Play 控制台中没有看到任何崩溃报告,但我们怀疑在 CryptoObject 生成期间出现了问题。

因此,如果 CryptoObject 可以替换为 null,我们很乐意这样做以简化我们的代码。

我是否需要 CryptoObject 对象,或者在 FingerprintManager.authenticate

期间对于以下用例为 null
/**
 * Small helper class to manage text/icon around fingerprint authentication UI.
 */
public class FingerprintUiHelper extends FingerprintManagerCompat.AuthenticationCallback {
    private static final String TAG = "FingerprintUiHelper";

    private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
    private static final String DEFAULT_KEY_NAME = "hello world key name";

    private int configShortAnimTime;

    private final FingerprintManagerCompat mFingerprintManager;
    private final ImageView mIcon;
    private final Callback mCallback;
    private CancellationSignal mCancellationSignal;

    private boolean mSelfCancelled;

    private final ResetErrorRunnable resetErrorRunnable = new ResetErrorRunnable();

    private final int mSuccessColor;
    private final int mAlertColor;

    private class ResetErrorRunnable implements Runnable {

        @Override
        public void run() {
            resetError();
        }
    }

    public static FingerprintUiHelper newInstance(ImageView icon, Callback callback, int successColor, int alertColor) {
        FingerprintManagerCompat fingerprintManagerCompat = FingerprintManagerCompat.from(WeNoteApplication.instance());
        return new FingerprintUiHelper(fingerprintManagerCompat, icon, callback, successColor, alertColor);
    }

    private void initResource() {
        configShortAnimTime = WeNoteApplication.instance().getResources().getInteger(android.R.integer.config_shortAnimTime);
    }

    /**
     * Constructor for {@link FingerprintUiHelper}.
     */
    private FingerprintUiHelper(FingerprintManagerCompat fingerprintManager,
                        ImageView icon, Callback callback, int successColor, int alertColor) {
        initResource();

        mFingerprintManager = fingerprintManager;
        mIcon = icon;
        mCallback = callback;
        mSuccessColor = successColor;
        mAlertColor = alertColor;
    }

    public boolean isFingerprintAuthAvailable() {
        // The line below prevents the false positive inspection from Android Studio
        // noinspection ResourceType
        return mFingerprintManager.isHardwareDetected()
                && mFingerprintManager.hasEnrolledFingerprints();
    }

    /**
     * Initialize the {@link Cipher} instance with the created key in the
     * {@link #createKey(String, boolean)} method.
     *
     * @param keyName the key name to init the cipher
     * @return {@code true} if initialization is successful, {@code false} if the lock screen has
     * been disabled or reset after the key was generated, or if a fingerprint got enrolled after
     * the key was generated.
     */
    private boolean initCipher(Cipher cipher, String keyName) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            return false;
        }

        KeyStore keyStore;
        KeyGenerator keyGenerator;

        try {
            keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
        } catch (KeyStoreException e) {
            Log.e(TAG, "", e);
            return false;
        }

        try {
            keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
        } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
            Log.e(TAG, "", e);
            return false;
        }

        // The enrolling flow for fingerprint. This is where you ask the user to set up fingerprint
        // for your flow. Use of keys is necessary if you need to know if the set of
        // enrolled fingerprints has changed.
        try {
            keyStore.load(null);
            // Set the alias of the entry in Android KeyStore where the key will appear
            // and the constrains (purposes) in the constructor of the Builder

            KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(keyName,
                    KeyProperties.PURPOSE_ENCRYPT |
                            KeyProperties.PURPOSE_DECRYPT)
                    .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                    // Require the user to authenticate with a fingerprint to authorize every use
                    // of the key
                    .setUserAuthenticationRequired(true)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);

            // This is a workaround to avoid crashes on devices whose API level is < 24
            // because KeyGenParameterSpec.Builder#setInvalidatedByBiometricEnrollment is only
            // visible on API level +24.
            // Ideally there should be a compat library for KeyGenParameterSpec.Builder but
            // which isn't available yet.
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                builder.setInvalidatedByBiometricEnrollment(true);
            }
            keyGenerator.init(builder.build());
            keyGenerator.generateKey();
        } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
                | CertificateException | IOException e) {
            Log.e(TAG, "", e);
            return false;
        }

        try {
            keyStore.load(null);
            SecretKey key = (SecretKey) keyStore.getKey(keyName, null);
            cipher.init(Cipher.ENCRYPT_MODE, key);
            return true;
        } catch (Exception e) {
            Log.e(TAG, "", e);
            return false;
        }
    }

    public void startListening() {
        if (!isFingerprintAuthAvailable()) {
            return;
        }

        Cipher defaultCipher;
        try {
            defaultCipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
                    + KeyProperties.BLOCK_MODE_CBC + "/"
                    + KeyProperties.ENCRYPTION_PADDING_PKCS7);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            Log.e(TAG, "", e);
            return;
        }

        if (false == initCipher(defaultCipher, DEFAULT_KEY_NAME)) {
            return;
        }

        FingerprintManagerCompat.CryptoObject cryptoObject = new FingerprintManagerCompat.CryptoObject(defaultCipher);

        startListening(cryptoObject);

        showIcon();
    }

    private void startListening(FingerprintManagerCompat.CryptoObject cryptoObject) {
        if (!isFingerprintAuthAvailable()) {
            return;
        }
        mCancellationSignal = new CancellationSignal();
        mSelfCancelled = false;

        // The line below prevents the false positive inspection from Android Studio
        // noinspection ResourceType
        mFingerprintManager
                .authenticate(cryptoObject, 0 /* flags */, mCancellationSignal, this, null);
    }

    public void stopListening() {
        if (mCancellationSignal != null) {
            mSelfCancelled = true;
            mCancellationSignal.cancel();
            mCancellationSignal = null;
        }
    }

    @Override
    public void onAuthenticationError(int errMsgId, CharSequence errString) {
        if (!mSelfCancelled) {
            if (errMsgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) {
                mIcon.removeCallbacks(resetErrorRunnable);
                showError();
                return;
            }

            if (errMsgId == FingerprintManager.FINGERPRINT_ACQUIRED_TOO_FAST) {
                return;
            }

            showError();
            mIcon.postDelayed(resetErrorRunnable, configShortAnimTime);
        }
    }

    @Override
    public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
        showError();
        mIcon.postDelayed(resetErrorRunnable, configShortAnimTime);
    }

    @Override
    public void onAuthenticationFailed() {
        showError();
        mIcon.postDelayed(resetErrorRunnable, configShortAnimTime);
    }

    @Override
    public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
        mIcon.setColorFilter(mSuccessColor);

        mIcon.postDelayed(() -> mCallback.onAuthenticated(), configShortAnimTime);
    }

    private void showIcon() {
        mIcon.setVisibility(View.VISIBLE);
    }

    private void showError() {
        mIcon.setColorFilter(mAlertColor);
    }

    private void resetError() {
        mIcon.clearColorFilter();
    }

    public interface Callback {

        void onAuthenticated();
    }
}

是否需要 CryptoObject 取决于您是否要执行要求用户使用指纹进行身份验证的加密操作。只有你知道答案。


例如,假设您的应用程序与服务器进行通信,并且在某些时候您想向服务器证明用户已在您的应用程序中使用他们的指纹进行身份验证。

您可能会这样做的方式是,当用户首先 "registers" 在您的应用程序中(无论如何完成),您创建一个需要指纹身份验证的 RSA 密钥对,然后共享 public 与服务器的密钥。

以后,当你想向服务器证明用户已经通过身份验证时,你可以向服务器请求一些数据来签名。然后,您使用 RSA 私钥创建 Signature,并将其包装到 CryptoObject 中。用户通过身份验证后,您可以对从服务器获得的数据进行签名,并将签名发送到服务器,服务器可以使用 public 密钥验证签名。

这比仅仅说 "the fingerprint authentication succeeded" 增加了额外的安全级别,因为 - 除非设备上存在一些严重的安全漏洞 - 私钥在用户已通过身份验证,即使在已获得 root 权限的设备上也是如此。