web-dev-qa-db-ja.com

Android Fingerprint APIの暗号化と復号化

Android M Fingerprint APIを使用して、ユーザーがアプリケーションにログインできるようにします。これを行うには、ユーザー名とパスワードをデバイスに保存する必要があります。現在、Fingerprint APIと同様にログインが機能していますが、ユーザー名とパスワードは両方ともプレーンテキストとして保存されています。パスワードを保存する前に暗号化し、ユーザーが指紋で認証した後に取得できるようにします。

これを機能させるのに非常に苦労しています。 Android Securityサンプル からできることを適用しようとしていますが、各例は暗号化または署名のみを処理し、復号化はしないようです。

これまでのところ、非対称暗号化を使用してAndroidの使用を許可するAndroidKeyStoreKeyPairGenerator、およびCipherのインスタンスを取得する必要があります- KeyGenParameterSpec.Builder().setUserAuthenticationRequired(true) 。非対称暗号化の理由は、ユーザーが認証されていない場合、setUserAuthenticationRequiredメソッドがanyキーの使用をブロックするためですが、

この承認は、秘密鍵と秘密鍵の操作にのみ適用されます。公開鍵操作は制限されていません。

これにより、ユーザーが指紋で認証される前に公開キーを使用してパスワードを暗号化し、ユーザーが認証された後にのみ秘密キーを使用して復号化できます。

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でプレーンテキストを暗号化して、ユーザーがFingerprint APIで認証された後に復号化できるようにするにはどうすればよいですか?


主に KeyGenParameterSpec ドキュメントページで情報が発見されたため、私はある程度進歩しました。

getKeyStoreencryptePassworddecryptPasswordgetKeyPairGenerator、およびgetCipherはほとんど同じですが、KeyPairGenerator.getInstanceおよびCipher.getInstanceをそれぞれ"RSA"および"RSA/ECB/OAEPWithSHA-256AndMGF1Padding"に変更しました。

また、私は理解していることから、Java 1.7(したがってAndroid)はECでの暗号化と復号化をサポートしていないため、残りのコードを楕円曲線ではなくRSAに変更しました。ドキュメントページの「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);
    }
}

initCipherドキュメントの既知の問題に基づいてKeyGenParameterSpecメソッドも変更しました。

Android 6.0(APIレベル23)の既知のバグにより、公開キーに対してもユーザー認証関連の承認が強制されます。この問題を回避するには、Androidキーストアの外部で使用する公開キー素材を抽出します。

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

これで、パスワードを暗号化して、暗号化されたパスワードを保存できます。しかし、暗号化されたパスワードを取得して解読しようとすると、KeyStoreExceptionUnknown error...

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)
41
Bryan

Android Issue Tracker でパズルの最後のピースを見つけました。別の既知のバグにより、OAEPを使用するときに、無制限のPublicKeyCipherと互換性がなくなります。回避策は、初期化時に新しいOAEPParameterSpecCipherに追加することです。

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