背景
APP中需要保存一些用户的个人信息到本地,比如:手机号、身份证、盐之类的,按要求不得明文存储。
在早期方案中使用AES进行加解密,密钥拆分成几个部分,各自进行base64、循环位移等方式,然后在APP运行时进行解码、拼装的形式避免APP被人从源码中直接翻出密钥。
最近了解到Android系统有个KeyStore的API可以实现,相比现有方案有以下优点:
- 代码中彻底不出现密钥的文本,杜绝反编译然后从源码中一点点找出密钥的风险。
- 每个用户的每次安装均为不同的密钥,即使有用户的密钥被破解并泄露,也不影响其他用户,该用户也只需(清除APP数据/卸载重装)即可获得新的密钥。
缺点则是:
- 仅适用于本地的信息加密,因为开发者自己都不知道密钥会是什么,无法与服务端互相约定。
下面以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_CBC
、KeyProperties.ENCRYPTION_PADDING_PKCS7
一一对应。
加密
加密过程如图所示,正常执行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);
}
}
解密
解密过程如图所示:对待解密的文本取出字节码之后,进行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)