【问题标题】:Storing credentials in the Android app在 Android 应用程序中存储凭据
【发布时间】:2017-07-22 12:00:35
【问题描述】:

我们如何安全地存储凭据数据以访问 Android 应用中的 smtp 服务器?这些数据是常数,只有开发人员应该知道它们。目前它们存储在代码中,但这并不安全,因为它们可以通过反编译应用程序来查看。

是否可以为此目的使用 Android 密钥库系统以及如何使用?最重要的是,Android Keystore 会在 root 设备上有效吗?

【问题讨论】:

    标签: android encryption root credentials android-keystore


    【解决方案1】:

    在 android 应用程序中,您可以将数据存储在 SharedPreferences 中,但由于这些数据实际上存储在一个文件中,任何对手机具有 root 访问权限的人都可以访问它。如果您想存储凭据或任何其他敏感数据,这意味着安全漏洞。

    为了避免其他人以纯文本形式看到此数据,解决方案是在存储数据之前对其进行加密。从 API 18 开始,Android 引入了 KeyStore,它能够存储用于加密和解密数据的密钥。

    在 API 23 之前的问题是您无法将 AES 密钥存储在 KeyStore 中,因此最可靠的加密密钥是带有私钥和公钥的 RSA。

    所以我想出的解决方案是:

    适用于 23 岁以下的 API

    • 您生成 RSA 私钥和公钥并将其保存在 KeyStore 中,生成 AES 密钥,使用 RSA 公钥对其进行加密并将其保存到 SharedPreferences。
    • 每次您需要使用 AES 密钥将加密数据保存在 SharedPreferences 中时,您都会从 SharedPreferences 获取加密的 AES 密钥,使用 RSA 私钥对其进行解密,并使用已解密的 AES 密钥对您想要保存到 SharedPreferences 的数据进行加密。
    • 要解密数据,过程几乎相同,从 SharedPreferences 获取加密的 AES 密钥,使用 RSA 私钥对其进行解密,从 SharedPreferences 获取要解密的加密数据,然后使用解密的 AES 密钥对其进行解密。

    适用于 API 23 及更高版本

    • 只需生成一个 AES 密钥并将其存储在 KeyStore 中,并在需要时随时访问它以进行数据加密/解密。

    还为加密添加了生成的 IV。

    代码:

    public class KeyHelper{
    
    
        private static final String RSA_MODE =  "RSA/ECB/PKCS1Padding";
        private static final String AES_MODE_M = "AES/GCM/NoPadding";
    
        private static final String KEY_ALIAS = "KEY";
        private static final String AndroidKeyStore = "AndroidKeyStore";
        public static final String SHARED_PREFENCE_NAME = "SAVED_TO_SHARED";
        public static final String ENCRYPTED_KEY = "ENCRYPTED_KEY";
        public static final String PUBLIC_IV = "PUBLIC_IV";
    
    
        private KeyStore keyStore;
        private static KeyHelper keyHelper;
    
        public static KeyHelper getInstance(Context ctx){
            if(keyHelper == null){
                try{
                    keyHelper = new KeyHelper(ctx);
                } catch (NoSuchPaddingException | NoSuchProviderException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | KeyStoreException | CertificateException | IOException e){
                    e.printStackTrace();
                }
            }
            return keyHelper;
        }
    
        public KeyHelper(Context ctx) throws  NoSuchPaddingException,NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, KeyStoreException, CertificateException, IOException  {
            this.generateEncryptKey(ctx);
            this.generateRandomIV(ctx);
            if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M){
                try{
                    this.generateAESKey(ctx);
                } catch(Exception e){
                    e.printStackTrace();
                }
            }
        }
    
    
        private void generateEncryptKey(Context ctx) throws  NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, KeyStoreException, CertificateException, IOException {
    
            keyStore = KeyStore.getInstance(AndroidKeyStore);
            keyStore.load(null);
    
            if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M){
                if (!keyStore.containsAlias(KEY_ALIAS)) {
                    KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, AndroidKeyStore);
                    keyGenerator.init(
                            new KeyGenParameterSpec.Builder(KEY_ALIAS,
                                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                                    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                                    .setRandomizedEncryptionRequired(false)
                                    .build());
                    keyGenerator.generateKey();
                }
            } else{
                if (!keyStore.containsAlias(KEY_ALIAS)) {
                    // Generate a key pair for encryption
                    Calendar start = Calendar.getInstance();
                    Calendar end = Calendar.getInstance();
                    end.add(Calendar.YEAR, 30);
                    KeyPairGeneratorSpec spec = new   KeyPairGeneratorSpec.Builder(ctx)
                            .setAlias(KEY_ALIAS)
                            .setSubject(new X500Principal("CN=" + KEY_ALIAS))
                            .setSerialNumber(BigInteger.TEN)
                            .setStartDate(start.getTime())
                            .setEndDate(end.getTime())
                            .build();
                    KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, AndroidKeyStore);
                    kpg.initialize(spec);
                    kpg.generateKeyPair();
                }
            }
    
    
        }
    
        private byte[] rsaEncrypt(byte[] secret) throws Exception{
            KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(KEY_ALIAS, null);
            // Encrypt the text
            Cipher inputCipher = Cipher.getInstance(RSA_MODE, "AndroidOpenSSL");
            inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.getCertificate().getPublicKey());
    
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, inputCipher);
            cipherOutputStream.write(secret);
            cipherOutputStream.close();
    
            return outputStream.toByteArray();
        }
    
        private  byte[]  rsaDecrypt(byte[] encrypted) throws Exception {
            KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(KEY_ALIAS, null);
            Cipher output = Cipher.getInstance(RSA_MODE, "AndroidOpenSSL");
            output.init(Cipher.DECRYPT_MODE, privateKeyEntry.getPrivateKey());
            CipherInputStream cipherInputStream = new CipherInputStream(
                new ByteArrayInputStream(encrypted), output);
            ArrayList<Byte> values = new ArrayList<>();
            int nextByte;
            while ((nextByte = cipherInputStream.read()) != -1) {
                values.add((byte)nextByte);
            }
    
            byte[] bytes = new byte[values.size()];
            for(int i = 0; i < bytes.length; i++) {
                bytes[i] = values.get(i).byteValue();
            }
            return bytes;
        }
    
        private void generateAESKey(Context context) throws  Exception{
            SharedPreferences pref = context.getSharedPreferences(SHARED_PREFENCE_NAME, Context.MODE_PRIVATE);
            String enryptedKeyB64 = pref.getString(ENCRYPTED_KEY, null);
            if (enryptedKeyB64 == null) {
                byte[] key = new byte[16];
                SecureRandom secureRandom = new SecureRandom();
                secureRandom.nextBytes(key);
                byte[] encryptedKey = rsaEncrypt(key);
                enryptedKeyB64 = Base64.encodeToString(encryptedKey, Base64.DEFAULT);
                SharedPreferences.Editor edit = pref.edit();
                edit.putString(ENCRYPTED_KEY, enryptedKeyB64);
                edit.apply();
            }
        }
    
    
        private Key getAESKeyFromKS() throws  NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, KeyStoreException, CertificateException, IOException, UnrecoverableKeyException{
            keyStore = KeyStore.getInstance(AndroidKeyStore);
            keyStore.load(null);
            SecretKey key = (SecretKey)keyStore.getKey(KEY_ALIAS,null);
            return key;
        }
    
    
        private Key getSecretKey(Context context) throws Exception{
            SharedPreferences pref = context.getSharedPreferences(SHARED_PREFENCE_NAME, Context.MODE_PRIVATE);
            String enryptedKeyB64 = pref.getString(ENCRYPTED_KEY, null);
    
            byte[] encryptedKey = Base64.decode(enryptedKeyB64, Base64.DEFAULT);
            byte[] key = rsaDecrypt(encryptedKey);
            return new SecretKeySpec(key, "AES");
        }
    
        public String encrypt(Context context, String input) throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, BadPaddingException, IllegalBlockSizeException, UnsupportedEncodingException {
            Cipher c;
            SharedPreferences pref = context.getSharedPreferences(SHARED_PREFENCE_NAME, Context.MODE_PRIVATE);
            String publicIV = pref.getString(PUBLIC_IV, null);
    
            if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M){
                c = Cipher.getInstance(AES_MODE_M);
                try{
                    c.init(Cipher.ENCRYPT_MODE, getAESKeyFromKS(), new GCMParameterSpec(128,Base64.decode(publicIV, Base64.DEFAULT)));
                } catch(Exception e){
                    e.printStackTrace();
                }
            } else{
                c = Cipher.getInstance(AES_MODE_M);
                try{
                    c.init(Cipher.ENCRYPT_MODE, getSecretKey(context),new GCMParameterSpec(128,Base64.decode(publicIV, Base64.DEFAULT)));
                } catch (Exception e){
                    e.printStackTrace();
                }
            }
            byte[] encodedBytes = c.doFinal(input.getBytes("UTF-8"));
            return Base64.encodeToString(encodedBytes, Base64.DEFAULT);
        }
    
    
    
    
    
        public String decrypt(Context context, String encrypted) throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, BadPaddingException, IllegalBlockSizeException, UnsupportedEncodingException {
            Cipher c;
            SharedPreferences pref = context.getSharedPreferences(SHARED_PREFENCE_NAME, Context.MODE_PRIVATE);
            String publicIV = pref.getString(PUBLIC_IV, null);
    
    
            if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M){
                c = Cipher.getInstance(AES_MODE_M);
                try{
                    c.init(Cipher.DECRYPT_MODE, getAESKeyFromKS(), new GCMParameterSpec(128,Base64.decode(publicIV, Base64.DEFAULT)));
    
                } catch(Exception e){
                    e.printStackTrace();
                }
            } else{
                c = Cipher.getInstance(AES_MODE_M);
                try{
                    c.init(Cipher.DECRYPT_MODE, getSecretKey(context), new GCMParameterSpec(128,Base64.decode(publicIV, Base64.DEFAULT)));
                } catch (Exception e){
                    e.printStackTrace();
                }
            }
    
            byte[] decodedValue = Base64.decode(encrypted.getBytes("UTF-8"), Base64.DEFAULT);
            byte[] decryptedVal = c.doFinal(decodedValue);
            return new String(decryptedVal);
        }
    
        public void generateRandomIV(Context ctx){
            SharedPreferences pref = ctx.getSharedPreferences(SHARED_PREFENCE_NAME, Context.MODE_PRIVATE);
            String publicIV = pref.getString(PUBLIC_IV, null);
    
            if(publicIV == null){
                SecureRandom random = new SecureRandom();
                byte[] generated = random.generateSeed(12);
                String generatedIVstr = Base64.encodeToString(generated, Base64.DEFAULT);
                SharedPreferences.Editor edit = pref.edit();
                edit.putString(PUBLIC_IV_PERSONAL, generatedIVstr);
                edit.apply();
            }
        }
    
        private String getStringFromSharedPrefs(String key, Context ctx){
            SharedPreferences prefs = ctx.getSharedPreferences(MyConstants.APP_SHAREDPREFS, 0);
            return prefs.getString(key, null);
        }
    }
    

    注意:这仅适用于 API 18 及更高版本

    【讨论】:

    • 好的,谢谢您的回答。但是如何在应用创建阶段将访问数据保存在apk文件中?
    • 除了代码生成的数据之外,您不会在 apk 文件中保存任何数据
    • 那么我们签署应用程序的密钥呢?
    • 该密钥由 Google 提供,因此您可以签署 APK 以便能够将其上传到 Play 商店
    • 是否可以在我们的应用中使用签名的 apk 证书?
    【解决方案2】:

    关于您对根设备安全性的问题,我建议您阅读以下论文:

    Analysis of Secure Key Storage Solutions on Android

    【讨论】:

    • 也就是说,没有安全的数据存储方法在有根设备上?
    【解决方案3】:

    您可以加密 smtp 凭据并将加密值存储在本地应用程序空间中(例如,在共享首选项中)。用于加密的密钥可以存储在密钥库中。

    欲了解更多信息,请参阅:How Can I Use the Android KeyStore to securely store arbitrary strings?

    【讨论】:

    • 如果引用的问题答案是 OP 的问题,您可以将问题标记为重复。如果没有,请在答案中添加链接中的相关代码,并将链接添加为参考。
    • 感谢您的回答。我是否正确理解访问 Keysore 不需要密码?而且不能有第三方应用访问?
    • 那么,如何保证keystore中的数据加密呢?
    猜你喜欢
    • 1970-01-01
    • 2019-02-17
    • 1970-01-01
    • 2019-09-03
    • 2012-06-14
    • 1970-01-01
    • 2023-03-24
    • 1970-01-01
    • 2016-10-27
    相关资源
    最近更新 更多