简体   繁体   English

如何使用 Android KeyStore 安全地存储任意字符串?

[英]How Can I Use the Android KeyStore to securely store arbitrary strings?

I would like to be able securely store some sensitive strings in the Android KeyStore.我希望能够在 Android KeyStore 中安全地存储一些敏感字符串。 I get the strings from the server but I have a use case which requires me to persist them.我从服务器获取字符串,但我有一个用例需要我保留它们。 KeyStore will only allow access from the same UID as that assigned to my app, and it will encrypt the data with the device master password, so it's my understanding that I don't have to do any additional encryption to protect my data. KeyStore 将只允许从与分配给我的应用程序的 UID 相同的 UID 进行访问,并且它将使用设备主密码对数据进行加密,因此我的理解是我不必进行任何额外的加密来保护我的数据。 My trouble is, I'm missing something about how to write the data.我的问题是,我缺少有关如何写入数据的信息。 The code I have below works perfectly, as long as the call to KeyStore.store(null) is omitted.只要省略对 KeyStore.store(null) 的调用,我下面的代码就可以完美运行。 That code fails, and as long as I can't store the data after putting it to the KeyStore, then I can't persist it.那个代码失败了,只要我把数据放到 KeyStore 之后我不能存储它,那么我就不能持久化它。

I think I'm missing something about the KeyStore API, but I don't know what.我想我遗漏了一些关于 KeyStore API 的东西,但我不知道是什么。 Any help appreciated!任何帮助表示赞赏!

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

I started with the premise that I could use AndroidKeyStore to secure arbitrary blobs of data, and call them "keys".我的前提是我可以使用 AndroidKeyStore 来保护任意数据块,并将它们称为“密钥”。 However, the deeper I delved into this, the clearer it became that the KeyStore API is deeply entangled with Security-related objects: Certificates, KeySpecs, Providers, etc. It's not designed to store arbitrary data, and I don't see a straightforward path to bending it to that purpose.然而,我越深入研究,就越清楚地发现 KeyStore API 与安全相关的对象深深地纠缠在一起:证书、KeySpecs、Providers 等。将其弯曲到该目的的路径。

However, the AndroidKeyStore can be used to help me to secure my sensitive data.但是,AndroidKeyStore 可用于帮助我保护敏感数据。 I can use it to manage the cryptographic keys which I will use to encrypt data local to the app.我可以用它来管理我将用来加密应用程序本地数据的加密密钥。 By using a combination of AndroidKeyStore, CipherOutputStream, and CipherInputStream, we can:通过结合使用 AndroidKeyStore、CipherOutputStream 和 CipherInputStream,我们可以:

  • Generate, securely store, and retrieve encryption keys on the device在设备上生成、安全存储和检索加密密钥
  • Encrypt arbitrary data and save it on the device (in the app's directory, where it will be further protected by the file system permissions)加密任意数据并将其保存在设备上(在应用程序的目录中,它将受到文件系统权限的进一步保护)
  • Access and decrypt the data for subsequent use.访问和解密数据以供后续使用。

Here is some example code which demonstrates how this is achieved.下面是一些示例代码,演示了如何实现这一点。

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

You may have noticed that there are problems handling different API levels with the Android Keystore.您可能已经注意到,使用 Android Keystore 处理不同的 API 级别存在问题。

Scytale is an open source library that provides a convenient wrapper around the Android Keystore so that you don't have write boiler plate and can dive straight into enryption/decryption. Scytale是一个开源库,它为 Android 密钥库提供了一个方便的包装器,这样您就无需编写样板,可以直接进入加密/解密。

Sample code:示例代码:

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

I have reworked the accepted answer by Patrick Brennan.我已经重新设计了 Patrick Brennan 接受的答案。 on Android 9, it was yielding a NoSuchAlgorithmException.在 Android 9 上,它产生了 NoSuchAlgorithmException。 The deprecated KeyPairGeneratorSpec has been replaced with KeyPairGenerator.已弃用的 KeyPairGeneratorSpec 已替换为 KeyPairGenerator。 There was also some work required to address an exception regarding the padding.还有一些工作需要解决有关填充的异常。

The code is annotated with the changes made: "***"代码注释了所做的更改:“***”

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

Note: “AndroidKeyStoreBCWorkaround” allows the code to work across different APIs.注意:“AndroidKeyStoreBCWorkaround”允许代码跨不同的 API 工作。

I would be grateful if anyone can comment on any shortcomings in this updated solution.如果有人可以评论此更新解决方案中的任何缺点,我将不胜感激。 Else if anyone with more Crypto knowledge feels confident to update Patrick's answer then I will remove this one.否则,如果有更多加密知识的人有信心更新帕特里克的答案,那么我将删除这个答案。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 如何安全地存储 Android KeyStore 密码 - How to store Android KeyStore passwords securely 如何安全保存我的密钥库 - How can I save my keystore securely 如何在Android上安全存储商店敏感日期? - How can I securely store store sensitive date on an Android? 如何在Android中安全存储EC私钥? - How can I securely store EC private key in Android? 如何将数据安全地存储在 Android 上,以便在重新安装后仍然有效? - How can I securely store data on Android that will survive a reinstall? React Native:如何在 CircleCI 中安全地存储 android 密钥库文件及其密码 - React Native: How to store android keystore file and its password securely in CircleCI 如何在Android Native Code中使用Keystore? - How can I use Keystore in Android Native Code? 如何安全地将图像和视频存储在android中,以便其他应用程序不能使用? - how to securely store a image and video in android so other application can not use? 如何将敏感数据(短字符串)安全地存储到密钥库中 - How securely store sensitive data (short string) into keystore 如何安全地在Android中存储密钥? - How to store secretkey in Android securely?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM