Шифрование и дешифрование Android Fingerprint API

Я использую Android M Fingerprint API, чтобы пользователи могли входить в приложение. Для этого мне нужно сохранить имя пользователя и пароль на устройстве. В настоящее время у меня работает вход в систему, а также API отпечатков пальцев, но имя пользователя и пароль хранятся в виде открытого текста. Я хотел бы зашифровать пароль перед его сохранением и иметь возможность получить его после аутентификации пользователя по отпечатку пальца.

Мне очень трудно заставить это работать. Я пытался применить все, что можно из образцов безопасности Android, но каждый пример кажется чтобы обрабатывать только шифрование или подпись, но никогда не расшифровывать.

На данный момент у меня есть то, что я должен получить экземпляр AndroidKeyStore, KeyPairGenerator и Cipher, используя асимметричную криптографию, чтобы разрешить использование Android _ 4_. Причина асимметричной криптографии заключается в том, что метод 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 и сделать его доступным для дешифрования после аутентификации пользователя с помощью Fingerprint API?


Я добился определенного прогресса, в основном благодаря обнаружению информации на KeyGenParameterSpec страницу документации.

Я оставил getKeyStore, encryptePassword, decryptPassword, getKeyPairGenerator и getCipher в основном одинаковыми, но я изменил KeyPairGenerator.getInstance и Cipher.getInstance на "RSA" и "RSA/ECB/OAEPWithSHA-256AndMGF1Padding" соответственно.

Я также изменил остальную часть кода на RSA вместо Elliptic Curve, потому что, насколько я понимаю, Java 1.7 (и, следовательно, Android) не поддерживает шифрование и дешифрование с помощью EC. Я изменил свой createKeyPair метод на основе примера «Пара ключей RSA для шифрования / дешифрования с использованием RSA OAEP» на странице документации:

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

person Bryan    schedule 14.03.2016    source источник
comment
Эй, не могли бы вы составить суть полного кода, необходимого для этого? Я искал решение этого около 2 недель.   -  person MichaelStoddart    schedule 23.11.2016
comment
@TheAndroidDev Честно говоря, я не очень хорошо отделял код пользовательского интерфейса от кода шифрования / дешифрования, а код основан на Dagger и RxJava, поэтому создание легко повторно используемой сути может быть нетривиальной задачей. Я посмотрю, что я могу придумать. Но пока большая часть кода (без Dagger) связана с другим моим вопросом: Как использовать неподдерживаемое исключение для версии с более низкой платформой .   -  person Bryan    schedule 23.11.2016


Ответы (1)


Я нашел последний кусочек головоломки в системе отслеживания проблем Android, еще одной известной причине ошибки. неограниченное PublicKey быть несовместимым с Cipher при использовании OAEP. Чтобы решить эту проблему, нужно добавить новый OAEPParameterSpec в Cipher при инициализации:

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);
    }
}
person Bryan    schedule 15.03.2016
comment
Привет, я перепробовал гораздо больше кода, и один из них - ваш. Я получаю сообщение об ошибке "Крипто-примитив не инициализирован". Сможете ли вы запустить код успешно? - person atasoyh; 08.06.2016
comment
@atasoyh Да, я использовал этот код. Ошибка, которую вы получаете, похоже, говорит о том, что Cipher не инициализирован. Вы уверены, что звоните initCipher(), прежде чем попытаетесь его использовать? - person Bryan; 08.06.2016
comment
Да, я позвонил в initChipper. Я перешел на реализацию AES. Я сейчас работаю с этим. - person atasoyh; 09.06.2016
comment
@atasoyh AES используется для симметричной криптографии, это показывает асимметричный пример с использованием RSA. Если вы хотите использовать AES, вам, возможно, придется значительно переработать код. - person Bryan; 09.06.2016
comment
Мне нужно шифрование, расшифровка. Сначала я решил использовать RSA, но не смог решить свою проблему и изменил свой код на AES. Спасибо за проявленный интерес к. - person atasoyh; 10.06.2016
comment
Какая последовательность обращений к процессу дешифрования? Что вы называете перед вызовом «дешифровать (шифровать)»? - person Caleb; 15.07.2016
comment
@Caleb Я звоню decrypt(cipher) после того, как получу ранее сгенерированный KeyPair из KeyStore, что было сделано в onAuthenticationSucceeded из FingerprintManagerCompat.AuthenticationCallback. - person Bryan; 15.07.2016
comment
Привет, Брайан, ты можешь добавить весь свой поток ?? Я получаю неинициализированный примитив Crypto - person WolfJee; 31.01.2017
comment
@WolfJee Как я уже говорил в другом комментарии, создание многоразового фрагмента кода на основе моей работы нетривиально; Я не лучшим образом справился с разделением кода. Хотя я работаю над этим, это не главный приоритет; но большая часть моего кода размещена в другом вопросе . В любом случае ошибка Crypto primitive not initialized означает, что mCipher.init() не вызывалась до mCipher.doFinal(). Если у вас все еще возникают проблемы, не стесняйтесь задавать вопрос и ссылаться на него здесь, я посмотрю. - person Bryan; 31.01.2017
comment
@Bryan: Спасибо за подробное объяснение и код. Шифрование и дешифрование работают нормально, если я нахожусь в одном сеансе. Однако, если я сгенерирую зашифрованный пароль, сохраню его в настройках и закрою приложение. Теперь, если я снова запускаю приложение и использую закрытый ключ для расшифровки пароля, я получаю неизвестную ошибку, которую вы получали. Итак, просто хотел спросить вас, пробовали ли вы сохранить и перезапустить приложение или все было сделано за один сеанс .... Большое спасибо - person Androidme; 13.02.2017
comment
@Androidme Все работает должным образом, даже если приложение закрыто и перезапущено. Похоже, вы могли генерировать новый PrivateKey при каждом запуске, заменяя PrivateKey, хранящийся в KeyStore. Но я не могу быть уверенным, не увидев кода. Задайте новый вопрос, и я посмотрю. - person Bryan; 13.02.2017
comment
Вы правы, @Bryan. Я генерировал новый PrivateKey. Сейчас он работает, но я могу расшифровать только одно значение. Если я сохраню имя пользователя и пароль и попытаюсь расшифровать оба, первый расшифровывает нормально, а второй дает ошибку android.security.KeyStoreException: ключевой пользователь не аутентифицирован. - person Androidme; 14.02.2017
comment
@Androidme Это ограничение API; setUserAuthenticationRequired(true) требует аутентификации при каждом использовании PrivateKey. Таким образом, пользователю потребуется повторно пройти аутентификацию, чтобы использовать ключ дважды. Простым решением для этого было бы объединить имя пользователя и пароль в один String, разделенный пробелом, и зашифровать / расшифровать их вместе. - person Bryan; 14.02.2017
comment
Вот что я сделал @Bryan. Спасибо за вашу помощь - person Androidme; 15.02.2017
comment
@atasoyh Мне было интересно, не могли бы вы поделиться своим кодом для использования AES вместо RSA в этом примере. Высоко оценен! Спасибо! - person A_Kiniyalocts; 06.03.2017
comment
@A_Kiniyalocts Как я уже говорил atasoyh, использование AES не будет простой заменой. Значительную часть моего кода пришлось бы переписать, чтобы использовать симметричную криптографию; и у меня нет ни времени, ни желания над этим работать. У Google есть пример приложения симметричного ключа с использованием AES, рекомендую взглянуть при этом. - person Bryan; 07.03.2017