Как я могу использовать Android KeyStore для безопасного хранения произвольных строк?

Я хотел бы иметь возможность безопасно хранить некоторые конфиденциальные строки в хранилище ключей Android. Я получаю строки с сервера, но у меня есть вариант использования, который требует от меня их сохранения. KeyStore будет разрешать доступ только с того же UID, который назначен моему приложению, и будет шифровать данные с помощью главного пароля устройства, поэтому, как я понимаю, мне не нужно выполнять какое-либо дополнительное шифрование для защиты моих данных. Моя проблема в том, что я что-то упускаю из виду, как записывать данные. Приведенный ниже код работает отлично, если опущен вызов KeyStore.store(null). Этот код дает сбой, и пока я не могу сохранить данные после помещения их в хранилище ключей, я не могу их сохранить.

Я думаю, что мне что-то не хватает в KeyStore API, но я не знаю что. Любая помощь приветствуется!

String metaKey = "ourSecretKey";
String encodedKey = "this is supposed to be a secret";
byte[] encodedKeyBytes = new byte[(int)encodedKey.length()];
encodedKeyBytes = encodedKey.getBytes("UTF-8");
KeyStoreParameter ksp = null;

//String algorithm = "DES";
String algorithm = "DESede";
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm);
SecretKeySpec secretKeySpec = new SecretKeySpec(encodedKeyBytes, algorithm);
SecretKey secretKey = secretKeyFactory.generateSecret(secretKeySpec);

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());

keyStore.load(null);

KeyStore.SecretKeyEntry secretKeyEntry = new KeyStore.SecretKeyEntry(secretKey);
keyStore.setEntry(metaKey, secretKeyEntry, ksp);

keyStore.store(null);

String recoveredSecret = "";
if (keyStore.containsAlias(metaKey)) {
    KeyStore.SecretKeyEntry recoveredEntry = (KeyStore.SecretKeyEntry)keyStore.getEntry(metaKey, ksp);
    byte[] bytes = recoveredEntry.getSecretKey().getEncoded();
    for (byte b : bytes) {
        recoveredSecret += (char)b;
     }
}
Log.v(TAG, "recovered " + recoveredSecret);

person Patrick Brennan    schedule 05.12.2014    source источник
comment
stackoverflow.com/a/67779409/6314955 проверьте это   -  person Malith Kuruwita    schedule 31.05.2021


Ответы (3)


Я переработал принятый ответ Патрика Бреннана. на Android 9 это приводило к исключению NoSuchAlgorithmException. Устаревший KeyPairGeneratorSpec был заменен на KeyPairGenerator. Также потребовалась некоторая работа по устранению исключения, касающегося заполнения.

Код аннотирован внесенными изменениями: "***"

@RequiresApi(api = Build.VERSION_CODES.M)
public static void storeExistingKey(Context context) {

    final String TAG = "KEY-UTIL";

    try {
        KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
        keyStore.load(null);

        String alias = "key11";
        int nBefore = keyStore.size();

        // Create the keys if necessary
        if (!keyStore.containsAlias(alias)) {

            Calendar notBefore = Calendar.getInstance();
            Calendar notAfter = Calendar.getInstance();
            notAfter.add(Calendar.YEAR, 1);


            // *** Replaced deprecated KeyPairGeneratorSpec with KeyPairGenerator
            KeyPairGenerator spec = KeyPairGenerator.getInstance(
                    // *** Specified algorithm here
                    // *** Specified: Purpose of key here
                    KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
            spec.initialize(new KeyGenParameterSpec.Builder(
                    alias, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT) 
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) //  RSA/ECB/PKCS1Padding
                    .setKeySize(2048)
                    // *** Replaced: setStartDate
                    .setKeyValidityStart(notBefore.getTime())
                    // *** Replaced: setEndDate
                    .setKeyValidityEnd(notAfter.getTime())
                    // *** Replaced: setSubject
                    .setCertificateSubject(new X500Principal("CN=test"))
                    // *** Replaced: setSerialNumber
                    .setCertificateSerialNumber(BigInteger.ONE)
                    .build());
            KeyPair keyPair = spec.generateKeyPair();
            Log.i(TAG, keyPair.toString());
        }

        int nAfter = keyStore.size();
        Log.v(TAG, "Before = " + nBefore + " After = " + nAfter);

        // Retrieve the keys
        KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null);
        PrivateKey privateKey = privateKeyEntry.getPrivateKey();
        PublicKey publicKey = privateKeyEntry.getCertificate().getPublicKey();


        Log.v(TAG, "private key = " + privateKey.toString());
        Log.v(TAG, "public key = " + publicKey.toString());

        // Encrypt the text
        String plainText = "This text is supposed to be a secret!";
        String dataDirectory = context.getApplicationInfo().dataDir;
        String filesDirectory = context.getFilesDir().getAbsolutePath();
        String encryptedDataFilePath = filesDirectory + File.separator + "keep_yer_secrets_here";

        Log.v(TAG, "plainText = " + plainText);
        Log.v(TAG, "dataDirectory = " + dataDirectory);
        Log.v(TAG, "filesDirectory = " + filesDirectory);
        Log.v(TAG, "encryptedDataFilePath = " + encryptedDataFilePath);

        // *** Changed the padding type here and changed to AndroidKeyStoreBCWorkaround
        Cipher inCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidKeyStoreBCWorkaround");
        inCipher.init(Cipher.ENCRYPT_MODE, publicKey);

        // *** Changed the padding type here and changed to AndroidKeyStoreBCWorkaround
        Cipher outCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidKeyStoreBCWorkaround");
        outCipher.init(Cipher.DECRYPT_MODE, privateKey);

        CipherOutputStream cipherOutputStream =
                new CipherOutputStream(
                        new FileOutputStream(encryptedDataFilePath), inCipher);
        // *** Replaced string literal with StandardCharsets.UTF_8
        cipherOutputStream.write(plainText.getBytes(StandardCharsets.UTF_8));
        cipherOutputStream.close();

        CipherInputStream cipherInputStream =
                new CipherInputStream(new FileInputStream(encryptedDataFilePath),
                        outCipher);
        byte[] roundTrippedBytes = new byte[1000]; // TODO: dynamically resize as we get more data

        int index = 0;
        int nextByte;
        while ((nextByte = cipherInputStream.read()) != -1) {
            roundTrippedBytes[index] = (byte) nextByte;
            index++;
        }

        // *** Replaced string literal with StandardCharsets.UTF_8
        String roundTrippedString = new String(roundTrippedBytes, 0, index, StandardCharsets.UTF_8);
        Log.v(TAG, "round tripped string = " + roundTrippedString);

    } catch (NoSuchAlgorithmException | UnsupportedOperationException | InvalidKeyException | NoSuchPaddingException | UnrecoverableEntryException | NoSuchProviderException | KeyStoreException | CertificateException | IOException e | InvalidAlgorithmParameterException e) {
        e.printStackTrace();
}

Примечание. «AndroidKeyStoreBCWorkaround» позволяет коду работать с разными API.

Я был бы признателен, если бы кто-нибудь мог прокомментировать какие-либо недостатки в этом обновленном решении. В противном случае, если кто-то, у кого больше знаний о криптографии, почувствует себя уверенно, чтобы обновить ответ Патрика, я удалю этот ответ.

person Elletlar    schedule 27.11.2019
comment
Можете ли вы объяснить, почему вы сохраняете текст в локальном хранилище, а не в хранилище ключей? - person Snipe3000; 23.04.2021
comment
Насколько я понимаю, цель хранилища ключей — просто безопасно хранить ключи на устройстве. Я не думаю, что он предназначен для хранения зашифрованных/защищенных данных. - person Elletlar; 23.04.2021

Я начал с предпосылки, что я могу использовать AndroidKeyStore для защиты произвольных блоков данных и называть их «ключами». Однако чем глубже я вникал в это, тем яснее становилось, что KeyStore API глубоко переплетен с объектами, связанными с безопасностью: сертификатами, KeySpecs, провайдерами и т. д. Он не предназначен для хранения произвольных данных, и я не вижу прямого путь к изгибу его для этой цели.

Однако AndroidKeyStore можно использовать для защиты моих конфиденциальных данных. Я могу использовать его для управления криптографическими ключами, которые я буду использовать для шифрования данных, локальных для приложения. Используя комбинацию AndroidKeyStore, CipherOutputStream и CipherInputStream, мы можем:

  • Создавайте, безопасно храните и извлекайте ключи шифрования на устройстве
  • Зашифруйте произвольные данные и сохраните их на устройстве (в каталоге приложения, где они будут дополнительно защищены разрешениями файловой системы)
  • Получите доступ и расшифруйте данные для последующего использования.

Вот пример кода, который демонстрирует, как это достигается.

try {
    KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
    keyStore.load(null);

    String alias = "key3";

    int nBefore = keyStore.size();

    // Create the keys if necessary
    if (!keyStore.containsAlias(alias)) {

        Calendar notBefore = Calendar.getInstance();
        Calendar notAfter = Calendar.getInstance();
        notAfter.add(Calendar.YEAR, 1);
        KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(this)
            .setAlias(alias)
            .setKeyType("RSA")
            .setKeySize(2048)
            .setSubject(new X500Principal("CN=test"))
            .setSerialNumber(BigInteger.ONE)
            .setStartDate(notBefore.getTime())
            .setEndDate(notAfter.getTime())
            .build();
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
        generator.initialize(spec);

        KeyPair keyPair = generator.generateKeyPair();
    }
    int nAfter = keyStore.size();
    Log.v(TAG, "Before = " + nBefore + " After = " + nAfter);

    // Retrieve the keys
    KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(alias, null);
    RSAPrivateKey privateKey = (RSAPrivateKey) privateKeyEntry.getPrivateKey();
    RSAPublicKey publicKey = (RSAPublicKey) privateKeyEntry.getCertificate().getPublicKey();

    Log.v(TAG, "private key = " + privateKey.toString());
    Log.v(TAG, "public key = " + publicKey.toString());

    // Encrypt the text
    String plainText = "This text is supposed to be a secret!";
    String dataDirectory = getApplicationInfo().dataDir;
    String filesDirectory = getFilesDir().getAbsolutePath();
    String encryptedDataFilePath = filesDirectory + File.separator + "keep_yer_secrets_here";

    Log.v(TAG, "plainText = " + plainText);
    Log.v(TAG, "dataDirectory = " + dataDirectory);
    Log.v(TAG, "filesDirectory = " + filesDirectory);
    Log.v(TAG, "encryptedDataFilePath = " + encryptedDataFilePath);

    Cipher inCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL");
    inCipher.init(Cipher.ENCRYPT_MODE, publicKey);

    Cipher outCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL");
    outCipher.init(Cipher.DECRYPT_MODE, privateKey);

    CipherOutputStream cipherOutputStream = 
        new CipherOutputStream(
            new FileOutputStream(encryptedDataFilePath), inCipher);
    cipherOutputStream.write(plainText.getBytes("UTF-8"));
    cipherOutputStream.close();

    CipherInputStream cipherInputStream = 
        new CipherInputStream(new FileInputStream(encryptedDataFilePath),
            outCipher);
    byte [] roundTrippedBytes = new byte[1000]; // TODO: dynamically resize as we get more data

    int index = 0;
    int nextByte;
    while ((nextByte = cipherInputStream.read()) != -1) {
        roundTrippedBytes[index] = (byte)nextByte;
        index++;
    }
    String roundTrippedString = new String(roundTrippedBytes, 0, index, "UTF-8");
    Log.v(TAG, "round tripped string = " + roundTrippedString);

} catch (NoSuchAlgorithmException e) {
    Log.e(TAG, Log.getStackTraceString(e));
} catch (NoSuchProviderException e) {
    Log.e(TAG, Log.getStackTraceString(e));
} catch (InvalidAlgorithmParameterException e) {
    Log.e(TAG, Log.getStackTraceString(e));
} catch (KeyStoreException e) {
    Log.e(TAG, Log.getStackTraceString(e));
} catch (CertificateException e) {
    Log.e(TAG, Log.getStackTraceString(e));
} catch (IOException e) {
    Log.e(TAG, Log.getStackTraceString(e));
} catch (UnrecoverableEntryException e) {
    Log.e(TAG, Log.getStackTraceString(e));
} catch (NoSuchPaddingException e) {
    Log.e(TAG, Log.getStackTraceString(e));
} catch (InvalidKeyException e) {
    Log.e(TAG, Log.getStackTraceString(e));
} catch (BadPaddingException e) {
    Log.e(TAG, Log.getStackTraceString(e));
} catch (IllegalBlockSizeException e) {
    Log.e(TAG, Log.getStackTraceString(e));
} catch (UnsupportedOperationException e) {
    Log.e(TAG, Log.getStackTraceString(e));
}
person Patrick Brennan    schedule 10.01.2015
comment
setKeyType требует API 19, а KeyPairGeneratorSpec — API 18. Как поддерживать API 18? - person Ilya Gazman; 18.10.2015
comment
Отличный пост. Мне очень помог. Мне пришлось удалить поставщика в вызове Cipher.getInstance(String transformation, Provider provider), чтобы заставить его работать, т.е. вместо этого вызвать Cipher.getInstance(String transformation). В противном случае вы получите java.security.InvalidKeyException с сообщением Need RSA private or public key. - person Adil Hussain; 28.01.2016
comment
Просто подумал, что задокументирую это где-нибудь, даже если я не смог найти его нигде в официальных документах. Учтите, что псевдоним ключа не может быть слишком длинным. Я не проверял длину, но я бился головой о стену, переключая все переключатели, пока не обнаружил, что строка псевдонима ключа слишком велика.. . - person Alamgir Mand; 14.01.2017
comment
Это именно то, что я искал. Сладкий! - person Diego Palomar; 10.08.2017
comment
Я получаю java.lang.ClassCastException: android.security.keystore.AndroidKeyStoreRSAPrivateKey cannot be cast to java.security.interfaces.RSAPrivateKey в телефонах Nougat (7.0) - person Geek Guy; 29.01.2019

Возможно, вы заметили, что в хранилище ключей Android возникают проблемы с обработкой разных уровней API.

Scytale – это библиотека с открытым исходным кодом, предоставляющая удобную оболочку для хранилища ключей Android, чтобы вам не приходилось писать шаблонную пластину и может сразу погрузиться в шифрование/дешифрование.

Образец кода:

// Create and save key
Store store = new Store(getApplicationContext());
if (!store.hasKey("test")) {
   SecretKey key = store.generateSymmetricKey("test", null);
}
...

// Get key
SecretKey key = store.getSymmetricKey("test", null);

// Encrypt/Decrypt data
Crypto crypto = new Crypto(Options.TRANSFORMATION_SYMMETRIC);
String text = "Sample text";

String encryptedData = crypto.encrypt(text, key);
Log.i("Scytale", "Encrypted data: " + encryptedData);

String decryptedData = crypto.decrypt(encryptedData, key);
Log.i("Scytale", "Decrypted data: " + decryptedData);
person David Rawson    schedule 24.08.2017
comment
Полезная библиотека, но, согласно ее документу, генерация симметричного ключа для API ‹23 подразумевает создание файла хранилища ключей в кеше приложения вместо использования AndroidKeyStore, поэтому вы теряете его безопасность ограничения доступа (файл хранилища ключей приложения легко извлекается). Рекомендуемый подход к работе с симметричными ключами в предыдущем API 23 заключается в создании этих ключей у другого поставщика и последующем сохранении их в зашифрованном виде с использованием асимметричных ключей, сгенерированных/сохраненных через AndroidKeyStore. - person giroxiii; 12.11.2018
comment
подробнее об androidkeystore и упаковке ключей proandroiddev.com/ - person Erlang Parasu; 29.06.2020