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

后端 未结 3 2171
Happy的楠姐
Happy的楠姐 2020-11-28 03:12

I would like to be able securely store some sensitive strings in the Android KeyStore. I get the strings from the server but I have a use case which requires me to persist t

相关标签:
3条回答
  • 2020-11-28 03:49

    You may have noticed that there are problems handling different API levels with the Android Keystore.

    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.

    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);
    
    0 讨论(0)
  • 2020-11-28 03:58

    I started with the premise that I could use AndroidKeyStore to secure arbitrary blobs of data, and call them "keys". 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.

    However, the AndroidKeyStore can be used to help me to secure my sensitive data. 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:

    • 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));
    }
    
    0 讨论(0)
  • 2020-11-28 04:03

    I have reworked the accepted answer by Patrick Brennan. on Android 9, it was yielding a NoSuchAlgorithmException. The deprecated KeyPairGeneratorSpec has been replaced with 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.

    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.

    0 讨论(0)
提交回复
热议问题