Android系统基于KeyStore的避免密钥硬编码方案

背景

APP中需要保存一些用户的个人信息到本地,比如:手机号、身份证、盐之类的,按要求不得明文存储。

在早期方案中使用AES进行加解密,密钥拆分成几个部分,各自进行base64、循环位移等方式,然后在APP运行时进行解码、拼装的形式避免APP被人从源码中直接翻出密钥。

最近了解到Android系统有个KeyStore的API可以实现,相比现有方案有以下优点:

  1. 代码中彻底不出现密钥的文本,杜绝反编译然后从源码中一点点找出密钥的风险。
  2. 每个用户的每次安装均为不同的密钥,即使有用户的密钥被破解并泄露,也不影响其他用户,该用户也只需(清除APP数据/卸载重装)即可获得新的密钥。

缺点则是:

  1. 仅适用于本地的信息加密,因为开发者自己都不知道密钥会是什么,无法与服务端互相约定。

下面以AES算法为例,对该方案的具体实现进行说明。

密钥的生成和取出

由于密钥的生成和保存、取出任务由安全硬件完成,所以我们需要一个媒介与安全硬件进行交互。 平时开发过程中,需要使用C++开发的.so文件组成的SDK才能与硬件交互。而这里我们可以理解为系统已经集成了这些SDK,我们只需要调用系统API即可完成交互。

下面的代码中,隐去了密钥的别名和默认的密钥

由于部分API从Android 6.0 API 23开始提供,所以对于更早版本的系统一律使用默认的密钥。

在取出密钥时,APP可能已经在安全硬件中保存过了,也可能没有保存过,所以需要先判断一下,如方法getKeyFromKeyStore()所示,有对应的密钥则直接取出就行,没有的话就需要生成一下createKeyFromKeyStore()

java 复制代码
public class KeyStoreUtil {
    /**
     * 密钥提供者
     */
    private static final String KEY_STORE_PROVIDER = "AndroidKeyStore";
    /**
     * 加解密算法
     */
    private static final String KEY_STORE_METHOD = "AES";
    /**
     * 密钥别名
     */
    private static final String KEY_STORE_ALIAS = "";

    /**
     * 默认的密钥
     */
    private static final String DEFAULT_ENCRYPTION_KEY = "";

    public static Key getKey() {
        if (Build.VERSION.SDK_INT < 23) {
            return getDefaultKey();
        }
        return getKeyFromKeyStore();
    }

    private static Key getDefaultKey() {
        try {
            SecretKeySpec key = new SecretKeySpec(DEFAULT_ENCRYPTION_KEY.getBytes(StandardCharsets.UTF_8),
                    KEY_STORE_METHOD);
            return key;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private static Key getKeyFromKeyStore() {
        if (Build.VERSION.SDK_INT < 23) {
            return null;
        }
        try {
            KeyStore keyStore = KeyStore.getInstance(KEY_STORE_PROVIDER);
            keyStore.load(null);
            Enumeration<String> alias = keyStore.aliases();
            if (alias.hasMoreElements() && keyStore.containsAlias(KEY_STORE_ALIAS)) {
                Log.d("lashglahsg", "keyStore里面有key");
                // keyStore里面有key
                KeyStore.ProtectionParameter protectionParameter = new KeyStore.PasswordProtection(null);
                KeyStore.SecretKeyEntry entry = (KeyStore.SecretKeyEntry) keyStore.getEntry(KEY_STORE_ALIAS, protectionParameter);
                return entry.getSecretKey();
            } else {
                Log.e("lashglahsg", "keyStore里面没有key");
                return createKeyFromKeyStore();
            }
        } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException | UnrecoverableEntryException e) {
            e.printStackTrace();
        }
        return getDefaultKey();
    }

    private static Key createKeyFromKeyStore() {
        if (Build.VERSION.SDK_INT < 23) {
            return null;
        }
        try {
            KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_STORE_METHOD, KEY_STORE_PROVIDER);
            KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(KEY_STORE_ALIAS,
                    KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT);
            builder.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                    .setUserAuthenticationRequired(false)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);
            keyGenerator.init(builder.build());
            return keyGenerator.generateKey();
        } catch (NoSuchAlgorithmException |
                InvalidAlgorithmParameterException |
                NoSuchProviderException e) {
            e.printStackTrace();
        }
        return getDefaultKey();
    }
}

加密和解密

加解密这里使用了另一个APICipher,获取实例时使用的"AES/CBC/PKCS7Padding"与KeyStore中生成密钥使用的"AES"KeyProperties.BLOCK_MODE_CBCKeyProperties.ENCRYPTION_PADDING_PKCS7一一对应。

加密

mindmap 加密保存的文本 合并起来Base64编码 ))AES的向量IV(( ))加密后的内容(( )明文内容(

加密过程如图所示,正常执行AES加密,同时加密向量IV,拼装起来,再进行Base64编码后保存

arduino 复制代码
public static String encrypt_AES(String src) {
    try {
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
        cipher.init(Cipher.ENCRYPT_MODE, ENCRYPTION_KEY_AES);
        final byte[] encrypted = cipher.doFinal(src.getBytes("UTF-8"));
        Log.d("lashglahsg", "iv长度:" + new String(cipher.getIV()).length());
        Log.d("lashglahsg", "iv:" + new String(cipher.getIV()));
        Log.d("lashglahsg", "iv数组长度:" + cipher.getIV().length);
        Log.d("lashglahsg", "iv数组:" + Arrays.toString(cipher.getIV()));
        Log.d("lashglahsg", "密文长度:" + Base64.encodeBytes(encrypted).length());
        Log.d("lashglahsg", "密文数组长度:" + encrypted.length);
        Log.d("lashglahsg", "密文数组:" + Arrays.toString(encrypted));
        final byte[] result = new byte[IV_LENGTH + encrypted.length];
        System.arraycopy(cipher.getIV(), 0, result, 0, IV_LENGTH);
        System.arraycopy(encrypted, 0, result, IV_LENGTH, encrypted.length);
        Log.d("lashglahsg", "结果数组长度:" + result.length);
        Log.d("lashglahsg", "结果数组:" + Arrays.toString(result));
        Log.d("lashglahsg", "----------");
        return Base64.encodeBytes(result);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

解密

mindmap 待解密的文本 取字节码 (Base64解码) ))AES的向量IV(( ))加密后的内容(( )解密后的文本(

解密过程如图所示:对待解密的文本取出字节码之后,进行Base64解码,再取出向量和密文内容,然后对密文正常解密即可。删除线部分是因为项目中原有代码就是如此,我为了新写方法的入参和返回能够一直也就这么做了,可以视项目实际情况变化。

vbnet 复制代码
public static String decrypt_AES(byte[] src) {
    Log.d("lashglahsg", "输入的数组长度:" + src.length);
    Log.d("lashglahsg", "输入的数组:" + Arrays.toString(src));
    try {
        byte[] srcFinal = Base64.decode(src);
        Log.d("lashglahsg", "解码的数组长度:" + srcFinal.length);
        Log.d("lashglahsg", "解码的数组:" + Arrays.toString(srcFinal));
        byte[] iv = new byte[IV_LENGTH];
        System.arraycopy(srcFinal, 0, iv, 0, IV_LENGTH);
        Log.d("lashglahsg", "iv长度:" + new String(iv).length());
        Log.d("lashglahsg", "iv:" + new String(iv));
        Log.d("lashglahsg", "iv数组长度:" + iv.length);
        Log.d("lashglahsg", "iv数组:" + Arrays.toString(iv));
        byte[] encrypted = new byte[srcFinal.length - IV_LENGTH];
        System.arraycopy(srcFinal, IV_LENGTH, encrypted, 0, encrypted.length);
        Log.d("lashglahsg", "密文长度:" + Base64.encodeBytes(encrypted).length());
        Log.d("lashglahsg", "密文数组长度:" + encrypted.length);
        Log.d("lashglahsg", "密文数组:" + Arrays.toString(encrypted));

        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
        cipher.init(Cipher.DECRYPT_MODE, ENCRYPTION_KEY_AES, makeIv(iv));
        String decrypted = new String(cipher.doFinal(encrypted));
        return decrypted;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

结果验证

csharp 复制代码
public static void test() {
    String str = "im FireInStone 666 233 %,./<>?;:'"[]{}\|!@#$%^&*()`~";
    Log.d("lashglahsg", "原文:" + str);
    String encrypt = AESUtil.encrypt_AES(str);
    Log.d("lashglahsg", "加密:" + encrypt);
    String decrypt = AESUtil.decrypt_AES(encrypt.getBytes());
    Log.d("lashglahsg", "解密:" + decrypt);
}

首次

非首次

参考文档

Android 密钥库系统 | App quality | Android Developers (google.cn)

相关推荐
Smartdaili China41 分钟前
如何在 Microsoft Edge 中设置代理: 快速而简单的方法
前端·爬虫·安全·microsoft·edge·社交·动态住宅代理
儒道易行2 小时前
【DVWA】RCE远程命令执行实战
网络·安全·网络安全
Hacker_LaoYi2 小时前
网络安全与加密
安全·web安全
Koi慢热3 小时前
路由基础(全)
linux·网络·网络协议·安全
hzyyyyyyyu4 小时前
内网安全隧道搭建-ngrok-frp-nps-sapp
服务器·网络·安全
网络研究院4 小时前
国土安全部发布关键基础设施安全人工智能框架
人工智能·安全·框架·关键基础设施
Daniel 大东6 小时前
BugJson因为json格式问题OOM怎么办
java·安全
EasyNVR10 小时前
NVR管理平台EasyNVR多个NVR同时管理:全方位安防监控视频融合云平台方案
安全·音视频·监控·视频监控
黑客Ash13 小时前
【D01】网络安全概论
网络·安全·web安全·php
阿龟在奔跑14 小时前
引用类型的局部变量线程安全问题分析——以多线程对方法局部变量List类型对象实例的add、remove操作为例
java·jvm·安全·list