Android指纹API加解密

Posted

技术标签:

【中文标题】Android指纹API加解密【英文标题】:Android Fingerprint API Encryption and Decryption 【发布时间】:2016-06-29 19:04:41 【问题描述】:

我正在使用 android M 指纹 API 来允许用户登录应用程序。为此,我需要将用户名和密码存储在设备上。目前我有登录工作,以及指纹 API,但用户名和密码都存储为明文。我想在存储密码之前对其进行加密,并在用户使用他们的指纹进行身份验证后能够检索它。

我很难让它发挥作用。我一直在尝试从Android Security samples 中应用我所能做的,但每个示例似乎只处理加密或签名,而从不解密。

到目前为止,我必须获得AndroidKeyStoreKeyPairGeneratorCipher 的实例,使用非对称加密技术以允许使用Android KeyGenParameterSpec.Builder().setUserAuthenticationRequired(true)。非对称加密的原因是,如果用户未通过身份验证,setUserAuthenticationRequired 方法将阻止任何密钥的使用,但是:

此授权仅适用于密钥和私钥操作。公钥操作不受限制。

这应该允许我在用户使用指纹进行身份验证之前使用公钥加密密码,然后仅在用户通过身份验证后使用私钥解密。

public KeyStore getKeyStore() 
    try 
        return KeyStore.getInstance("AndroidKeyStore");
     catch (KeyStoreException exception) 
        throw new RuntimeException("Failed to get an instance of KeyStore", exception);
    


public KeyPairGenerator getKeyPairGenerator() 
    try 
        return KeyPairGenerator.getInstance("EC", "AndroidKeyStore");
     catch(NoSuchAlgorithmException | NoSuchProviderException exception) 
        throw new RuntimeException("Failed to get an instance of KeyPairGenerator", exception);
    


public Cipher getCipher() 
    try 
        return Cipher.getInstance("EC");
     catch(NoSuchAlgorithmException | NoSuchPaddingException exception) 
        throw new RuntimeException("Failed to get an instance of Cipher", exception);
    


private void createKey() 
    try 
        mKeyPairGenerator.initialize(
                new KeyGenParameterSpec.Builder(KEY_ALIAS,
                        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                        .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1")
                        .setUserAuthenticationRequired(true)
                        .build());
        mKeyPairGenerator.generateKeyPair();
     catch(InvalidAlgorithmParameterException exception) 
        throw new RuntimeException(exception);
    


private boolean initCipher(int opmode) 
    try 
        mKeyStore.load(null);

        if(opmode == Cipher.ENCRYPT_MODE) 
            PublicKey key = mKeyStore.getCertificate(KEY_ALIAS).getPublicKey();
            mCipher.init(opmode, key);
         else 
            PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_ALIAS, null);
            mCipher.init(opmode, key);
        

        return true;
     catch (KeyPermanentlyInvalidatedException exception) 
        return false;
     catch(KeyStoreException | CertificateException | UnrecoverableKeyException
            | IOException | NoSuchAlgorithmException | InvalidKeyException
            | InvalidAlgorithmParameterException exception) 
        throw new RuntimeException("Failed to initialize Cipher", exception);
    


private void encrypt(String password) 
    try 
        initCipher(Cipher.ENCRYPT_MODE);
        byte[] bytes = mCipher.doFinal(password.getBytes());
        String encryptedPassword = Base64.encodeToString(bytes, Base64.NO_WRAP);
        mPreferences.getString("password").set(encryptedPassword);
     catch(IllegalBlockSizeException | BadPaddingException exception) 
        throw new RuntimeException("Failed to encrypt password", exception);
    


private String decryptPassword(Cipher cipher) 
    try 
        String encryptedPassword = mPreferences.getString("password").get();
        byte[] bytes = Base64.decode(encryptedPassword, Base64.NO_WRAP);
        return new String(cipher.doFinal(bytes));
     catch (IllegalBlockSizeException | BadPaddingException exception) 
        throw new RuntimeException("Failed to decrypt password", exception);
    

说实话,我不确定这是否正确,这是我能找到的关于该主题的所有内容的零碎。我更改的所有内容都会引发不同的异常,并且此特定构建不会运行,因为我无法实例化Cipher,它会引发NoSuchAlgorithmException: No provider found for EC。我也尝试过切换到RSA,但我遇到了类似的错误。

所以我的问题基本上是这样的;如何在 Android 上加密明文,并在用户通过指纹 API 身份验证后使其可用于解密?


我取得了一些进展,主要是因为发现了KeyGenParameterSpec 文档页面上的信息。

我保持getKeyStoreencryptePassworddecryptPasswordgetKeyPairGeneratorgetCipher 基本相同,但我将KeyPairGenerator.getInstanceCipher.getInstance 分别更改为"RSA""RSA/ECB/OAEPWithSHA-256AndMGF1Padding"

我还将其余代码更改为 RSA 而不是椭圆曲线,因为据我了解,Java 1.7(因此 Android)不支持使用 EC 进行加密和解密。我根据文档页面上的“使用 RSA OAEP 进行加密/解密的 RSA 密钥对”示例更改了我的 createKeyPair 方法:

private void createKeyPair() 
    try 
        mKeyPairGenerator.initialize(
                new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT)
                        .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
                        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
                        .setUserAuthenticationRequired(true)
                        .build());
        mKeyPairGenerator.generateKeyPair();
     catch(InvalidAlgorithmParameterException exception) 
        throw new RuntimeException(exception);
    

我还根据KeyGenParameterSpec 文档中的已知问题 更改了我的initCipher 方法:

Android 6.0(API 级别 23)中的一个已知错误会导致即使对公钥也强制执行与用户身份验证相关的授权。要解决此问题,请提取公钥材料以在 Android Keystore 之外使用。

private boolean initCipher(int opmode) 
    try 
        mKeyStore.load(null);

        if(opmode == Cipher.ENCRYPT_MODE) 
            PublicKey key = mKeyStore.getCertificate(KEY_ALIAS).getPublicKey();

            PublicKey unrestricted = KeyFactory.getInstance(key.getAlgorithm())
                    .generatePublic(new X509EncodedKeySpec(key.getEncoded()));

            mCipher.init(opmode, unrestricted);
         else 
            PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_ALIAS, null);
            mCipher.init(opmode, key);
        

        return true;
     catch (KeyPermanentlyInvalidatedException exception) 
        return false;
     catch(KeyStoreException | CertificateException | UnrecoverableKeyException
            | IOException | NoSuchAlgorithmException | InvalidKeyException
            | InvalidAlgorithmParameterException exception) 
        throw new RuntimeException("Failed to initialize Cipher", exception);
    

现在我可以加密密码,并保存加密后的密码。但是当我获得加密的密码并尝试解密时,我得到一个KeyStoreException 未知错误...

03-15 10:06:58.074 14702-14702/com.example.app E/LoginFragment: Failed to decrypt password
        javax.crypto.IllegalBlockSizeException
            at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:486)
            at javax.crypto.Cipher.doFinal(Cipher.java:1502)
            at com.example.app.ui.fragment.util.LoginFragment.onAuthenticationSucceeded(LoginFragment.java:251)
            at com.example.app.ui.controller.FingerprintCallback.onAuthenticationSucceeded(FingerprintCallback.java:21)
            at android.support.v4.hardware.fingerprint.FingerprintManagerCompat$Api23FingerprintManagerCompatImpl$1.onAuthenticationSucceeded(FingerprintManagerCompat.java:301)
            at android.support.v4.hardware.fingerprint.FingerprintManagerCompatApi23$1.onAuthenticationSucceeded(FingerprintManagerCompatApi23.java:96)
            at android.hardware.fingerprint.FingerprintManager$MyHandler.sendAuthenticatedSucceeded(FingerprintManager.java:805)
            at android.hardware.fingerprint.FingerprintManager$MyHandler.handleMessage(FingerprintManager.java:757)
            at android.os.Handler.dispatchMessage(Handler.java:102)
            at android.os.Looper.loop(Looper.java:148)
            at android.app.ActivityThread.main(ActivityThread.java:5417)
            at java.lang.reflect.Method.invoke(Native Method)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
        Caused by: android.security.KeyStoreException: Unknown error
            at android.security.KeyStore.getKeyStoreException(KeyStore.java:632)
            at android.security.keystore.KeyStoreCryptoOperationChunkedStreamer.doFinal(KeyStoreCryptoOperationChunkedStreamer.java:224)
            at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:473)
            at javax.crypto.Cipher.doFinal(Cipher.java:1502) 
            at com.example.app.ui.fragment.util.LoginFragment.onAuthenticationSucceeded(LoginFragment.java:251) 
            at com.example.app.ui.controller.FingerprintCallback.onAuthenticationSucceeded(FingerprintCallback.java:21) 
            at android.support.v4.hardware.fingerprint.FingerprintManagerCompat$Api23FingerprintManagerCompatImpl$1.onAuthenticationSucceeded(FingerprintManagerCompat.java:301) 
            at android.support.v4.hardware.fingerprint.FingerprintManagerCompatApi23$1.onAuthenticationSucceeded(FingerprintManagerCompatApi23.java:96) 
            at android.hardware.fingerprint.FingerprintManager$MyHandler.sendAuthenticatedSucceeded(FingerprintManager.java:805) 
            at android.hardware.fingerprint.FingerprintManager$MyHandler.handleMessage(FingerprintManager.java:757) 
            at android.os.Handler.dispatchMessage(Handler.java:102) 
            at android.os.Looper.loop(Looper.java:148) 
            at android.app.ActivityThread.main(ActivityThread.java:5417) 
            at java.lang.reflect.Method.invoke(Native Method) 
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) 
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

【问题讨论】:

嘿,您能否为执行此操作所需的完整代码创建一个要点?大约 2 周以来,我一直在寻找解决方案 @TheAndroidDev 说实话,我并没有很好地将 UI 代码与加密/解密代码分开,并且代码依赖于 Dagger 和 RxJava,因此制作一个易于重用的 gist 可能并非易事.我会看看我能想出什么。但就目前而言,大部分代码(无 Dagger)都在我的另一个问题中:How to Use Unsupported Exception for Lower Platform Version。 您能回答我的 SO 问题吗? ***.com/questions/40724749/… 【参考方案1】:

我在Android Issue Tracker 上找到了最后一块拼图,另一个已知错误导致使用 OAEP 时不受限制的PublicKeyCipher 不兼容。解决方法是在初始化时向Cipher 添加一个新的OAEPParameterSpec

OAEPParameterSpec spec = new OAEPParameterSpec(
        "SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT);

mCipher.init(opmode, unrestricted, spec);

下面是最终代码:

public KeyStore getKeyStore() 
    try 
        return KeyStore.getInstance("AndroidKeyStore");
     catch (KeyStoreException exception) 
        throw new RuntimeException("Failed to get an instance of KeyStore", exception);
    


public KeyPairGenerator getKeyPairGenerator() 
    try 
        return KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
     catch(NoSuchAlgorithmException | NoSuchProviderException exception) 
        throw new RuntimeException("Failed to get an instance of KeyPairGenerator", exception);
    


public Cipher getCipher() 
    try 
        return Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
     catch(NoSuchAlgorithmException | NoSuchPaddingException exception) 
        throw new RuntimeException("Failed to get an instance of Cipher", exception);
    


private void createKeyPair() 
    try 
        mKeyPairGenerator.initialize(
                new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT)
                        .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
                        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
                        .setUserAuthenticationRequired(true)
                        .build());
        mKeyPairGenerator.generateKeyPair();
     catch(InvalidAlgorithmParameterException exception) 
        throw new RuntimeException("Failed to generate key pair", exception);
    


private boolean initCipher(int opmode) 
    try 
        mKeyStore.load(null);

        if(opmode == Cipher.ENCRYPT_MODE) 
            PublicKey key = mKeyStore.getCertificate(KEY_ALIAS).getPublicKey();

            PublicKey unrestricted = KeyFactory.getInstance(key.getAlgorithm())
                    .generatePublic(new X509EncodedKeySpec(key.getEncoded()));

            OAEPParameterSpec spec = new OAEPParameterSpec(
                    "SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT);

            mCipher.init(opmode, unrestricted, spec);
         else 
            PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_ALIAS, null);
            mCipher.init(opmode, key);
        

        return true;
     catch (KeyPermanentlyInvalidatedException exception) 
        return false;
     catch(KeyStoreException | CertificateException | UnrecoverableKeyException
            | IOException | NoSuchAlgorithmException | InvalidKeyException
            | InvalidAlgorithmParameterException exception) 
        throw new RuntimeException("Failed to initialize Cipher", exception);
    


private void encrypt(String password) 
    try 
        initCipher(Cipher.ENCRYPT_MODE);
        byte[] bytes = mCipher.doFinal(password.getBytes());
        String encrypted = Base64.encodeToString(bytes, Base64.NO_WRAP);
        mPreferences.getString("password").set(encrypted);
     catch(IllegalBlockSizeException | BadPaddingException exception) 
        throw new RuntimeException("Failed to encrypt password", exception);
    


private String decrypt(Cipher cipher) 
    try 
        String encoded = mPreferences.getString("password").get();
        byte[] bytes = Base64.decode(encoded, Base64.NO_WRAP);
        return new String(cipher.doFinal(bytes));
     catch (IllegalBlockSizeException | BadPaddingException exception) 
        throw new RuntimeException("Failed to decrypt password", exception);
    

【讨论】:

嗨,我尝试了更多代码,其中之一就是您的代码。我收到一个错误“加密原语未初始化”。你能成功运行代码吗? 我需要加密、解密。我决定首先使用 RSA,但我无法解决我的问题,我将我的代码改为 AES。感谢您的关注。 @Caleb 在我从KeyStore 获得之前生成的KeyPair 之后,我正在调用decrypt(cipher),这是在FingerprintManagerCompat.AuthenticationCallbackonAuthenticationSucceeded 中完成的。 @Androidme 即使应用程序关闭并重新启动,一切都会按预期工作。听起来您可以在每次启动时生成一个新的PrivateKey,替换存储在KeyStore 中的PrivateKey。但是如果没有看到一些代码,我无法确定。问一个新问题,我会看看。 @Androidme 这是API的限制; setUserAuthenticationRequired(true) 需要对 PrivateKey 的每次使用进行身份验证。所以用户需要重新认证才能使用密钥两次。一个简单的解决方法是将用户名和密码连接成一个由空格分隔的String,并将它们一起加密/解密。

以上是关于Android指纹API加解密的主要内容,如果未能解决你的问题,请参考以下文章

Java加解密服务API解读

android中使用jni对字符串加解密实现分析

跪求 DES跨(C# Android IOS)三个平台通用的加解密方法

React中的AES加解密请求

Android-IO加解密核心与dex文件改造过程分析

Android-IO加解密核心与dex文件改造过程分析