Android指纹API加密和解密

时间:2022-08-22 18:23:24

I am using the Android M Fingerprint API to allow users to login to the application. To do this I would need to store the username and password on the device. Currently I have the login working, as well as the Fingerprint API, but the username and password are both stored as plaintext. I would like to encrypt the password before I store it, and be able to retrieve it after the user authenticates with their fingerprint.

我正在使用Android M指纹API来允许用户登录该应用程序。为此,我需要在设备上存储用户名和密码。目前我有登录工作,以及指纹API,但用户名和密码都存储为纯文本。我希望在存储密码之前加密密码,并且在用户使用指纹进行身份验证后能够检索密码。

I am having a great amount of difficulty getting this to work. I have been trying to apply what I can from the Android Security samples, but each example seems to only handle encryption or signing, and never decryption.

我正在努力解决这个问题。我一直试图从Android安全样本中应用我能做的,但每个例子似乎只处理加密或签名,而不是解密。

What I have so far is that I have to obtain an instance of the AndroidKeyStore, a KeyPairGenerator and a Cipher, using asymmetric cryptography to allow the use of the Android KeyGenParameterSpec.Builder().setUserAuthenticationRequired(true). The reason for asymmetric cryptography is because the setUserAuthenticationRequired method will block any use of the key if the user is not authenticated, but:

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

This authorization applies only to secret key and private key operations. Public key operations are not restricted.

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

This should allow me to encrypt the password using the public key before the user authenticates with their fingerprint, then decrypt using the private key only after the user is authenticated.

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

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);
    }
}

To be honest, I am not sure if any of this is right, it is bits and pieces from anything I could find on the subject. Everything I change throws a different exception, and this particular build does not run because I cannot instantiate the Cipher, it throws a NoSuchAlgorithmException: No provider found for EC. I have tried switch to RSA as well, but I get similar errors.

说实话,我不确定这是否正确,这是我能找到的关于这个主题的点点滴滴。我更改的所有内容都会抛出一个不同的异常,并且此特定构建不会运行,因为我无法实例化Cipher,它会抛出NoSuchAlgorithmException:找不到EC的提供程序。我也试过切换到RSA,但是我得到了类似的错误。

So my question is basically this; how can I encrypt plaintext on Android, and make it available for decryption after the user is authenticated by the Fingerprint API?

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


I have made some progress, mostly due to the discovery of the information on the KeyGenParameterSpec documentation page.

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

I have kept getKeyStore, encryptePassword, decryptPassword, getKeyPairGenerator and getCipher mostly the same, but I changed the KeyPairGenerator.getInstance and Cipher.getInstance to "RSA" and "RSA/ECB/OAEPWithSHA-256AndMGF1Padding" respectively.

我保持getKeyStore,encryptePassword,decryptPassword,getKeyPairGenerator和getCipher大致相同,但我分别将KeyPairGenerator.getInstance和Cipher.getInstance更改为“RSA”和“RSA / ECB / OAEPWithSHA-256AndMGF1Padding”。

I also changed the rest of the code to RSA instead of Elliptic Curve, because from what I understand, Java 1.7 (and therefore Android) does not support encryption and decryption with EC. I changed my createKeyPair method based on the "RSA key pair for encryption/decryption using RSA OAEP" example on the documentation page:

我还将其余代码更改为RSA而不是Elliptic Curve,因为根据我的理解,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);
    }
}

I also altered my initCipher method based on the known issue in the KeyGenParameterSpec documentation:

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

A known bug in Android 6.0 (API Level 23) causes user authentication-related authorizations to be enforced even for public keys. To work around this issue extract the public key material to use outside of Android Keystore.

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);
    }
}

Now I can encrypt the password, and save the encrypted password. But when I obtain the encrypted password and attempt to decrypt, I get a KeyStoreException Unknown error...

现在我可以加密密码,并保存加密密码。但是当我获得加密密码并尝试解密时,我收到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)

1 个解决方案

#1


30  

I found the final piece of the puzzle on the Android Issue Tracker, another known bug causes the unrestricted PublicKey to be incompatible with the Cipher when using OAEP. The work around is to add a new OAEPParameterSpec to the Cipher upon initialization:

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

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

mCipher.init(opmode, unrestricted, spec);

Below is the final code:

以下是最终代码:

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);
    }
}

#1


30  

I found the final piece of the puzzle on the Android Issue Tracker, another known bug causes the unrestricted PublicKey to be incompatible with the Cipher when using OAEP. The work around is to add a new OAEPParameterSpec to the Cipher upon initialization:

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

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

mCipher.init(opmode, unrestricted, spec);

Below is the final code:

以下是最终代码:

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);
    }
}