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)

相关推荐
l1x1n03 小时前
No.2 笔记 | 网络安全攻防:PC、CS工具与移动应用分析
安全·web安全
醉颜凉5 小时前
银河麒麟桌面操作系统V10 SP1:取消安装应用的安全授权认证
运维·安全·操作系统·国产化·麒麟·kylin os·安全授权认证
小小工匠9 小时前
Web安全 - 路径穿越(Path Traversal)
安全·web安全·路径穿越
不灭锦鲤12 小时前
ssrf学习(ctfhub靶场)
网络·学习·安全
网络研究院14 小时前
如何安全地大规模部署 GenAI 应用程序
网络·人工智能·安全·ai·部署·观点
DonciSacer18 小时前
TryHackMe 第6天 | Web Fundamentals (一)
安全
云卓科技1 天前
无人机之数据提取篇
科技·安全·机器人·无人机·制造
山兔11 天前
工控安全防护机制与技术
安全
HEX9CF1 天前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss
小小工匠1 天前
加密与安全_HOTP一次性密码生成算法
算法·安全·htop·一次性密码