背景
最近需要解决用户使用手机克隆进行手机备份,某些设备相关的数据,比如设备ID的的存储文件也被克隆,导致用户更换设备之后,从本地读取到的设备ID信息依旧是上个手机的(Android Q以及以上的设备,有时候没办法获取设备ID,只能给一个随机数,因此只能存储在应用本地),这样会导致安全问题。
KeyStore简介
利用 Android KeyStore System,您可以在容器中存储加密密钥,从而提高从设备中提取密钥的难度。在密钥进入密钥库后,可以将它们用于加密操作,而密钥材料仍不可导出。此外,它提供了密钥使用的时间和方式限制措施,例如要求进行用户身份验证才能使用密钥,或者限制为只能在某些加密模式中使用。
密钥库系统由 KeyChain API 以及在 Android 4.3(API 级别 18)中引入的 Android 密钥库提供程序功能使用。本文说明了何时以及如何使用 Android 密钥库提供程序。
注意:Android Keystore的API非线程安全,对于API的调用需要进行互斥操作。线程不安全的原因很简单,那就是不管上层创建多少个对象,底层都只对应同一个加解密硬件,硬件没有完成操作之前给出另一个操作命令会导致硬件工作异常。这也是官方文档 EncryptedFile 类和 EncryptedSharedPreferences 类中的方法不是线程安全的根本原因。
我们从 Google Tink 项目的讨论 Bug: using EncryptedSharedPreferences, it can cause crashes to users right when initializing it #535中可以了解到,目前三星Samsung的部分机型的底层实现就是线程不安全的。
更多的机型信息参考 Bug: using EncryptedSharedPreferences, it can cause crashes to users right when initializing it 崩溃率排行中,三星,小米遥遥领先!
应用场景
1、 存储密匙:Android提供的这个KeyStore最大的作用就是不需要开发者去维护这个密匙的存储问题,相比起存储在用户的数据空间或者是外部存储器都更加安全。需要注意:这个密匙随着用户清除数据或者卸载应用都会被清除掉。
2、得益于Android独立的一套密匙库系统,可以提高安全性
安全功能
Android 密钥库系统可以保护密钥材料免遭未经授权的使用。首先,Android 密钥库可以防止从应用进程和 Android 设备中整体提取密钥材料,从而避免了在 Android 设备之外以未经授权的方式使用密钥材料。其次,Android 密钥库可以让应用指定密钥的授权使用方式,并在应用进程之外强制实施这些限制,从而避免了在 Android 设备上以未经授权的方式使用密钥材料。
提取防范
Android 密钥库密钥使用两项安全措施来避免密钥材料被提取:
- 密钥材料永不进入应用进程。通过 Android 密钥库密钥执行加密操作时,应用会将待签署或验证的明文、密文和消息馈送到执行加密操作的系统进程。如果应用进程受攻击,攻击者也许能使用应用密钥,但无法提取密钥材料(例如,在 Android 设备以外使用)。
- 您可以将密钥材料绑定至 Android 设备的安全硬件,例如可信执行环境 (TEE) 和安全元素 (SE)。为密钥启用此功能时,其密钥材料永远不会暴露于安全硬件之外。如果 Android 操作系统受到攻击或者攻击者可以读取设备内部存储空间,攻击者也许能在 Android 设备上使用应用的 Android 密钥库,但无法从设备上提取这些数据。只有设备的安全硬件支持密钥算法、区块模式、填充方案和密钥有权使用的摘要的特定组合时,才可启用此功能。要检查是否为密钥启用了此功能,请获取密钥的 KeyInfo 并检查 KeyInfo.isInsideSecurityHardware() 的返回值。
密钥使用授权
为了避免在 Android 设备上以未经授权的方式使用密钥材料,在生成或导入密钥时 Android 密钥库会让应用指定密钥的授权使用方式。一旦生成或导入密钥,其授权将无法更改。然后,每次使用密钥时,都会由 Android 密钥库强制执行授权。这是一项高级安全功能,通常仅用于有以下要求的情形:在生成/导入密钥后(而不是之前或当中),应用进程受到攻击不会导致密钥以未经授权的方式使用。
支持的密钥使用授权可归为以下几个类别:
- 加密:授权密钥算法、运算或目的(加密、解密、签署、验证)、填充方案、区块模式以及可与密钥搭配使用的摘要;
- 时间有效性间隔:密钥获得使用授权的时间间隔;
- 用户身份验证:密钥只能在用户最近进行身份验证时使用。请参阅要求进行用户身份验证才能使用密钥。
作为一项额外的安全措施,对于密钥材料位于安全硬件内部的密钥(请参阅 KeyInfo.isInsideSecurityHardware()),某些密钥使用授权可能由安全硬件实施,具体取决于 Android 设备。加密和用户身份验证授权可能由安全硬件实施。由于安全硬件一般不具备独立的安全实时时钟,时间有效性间隔授权不可能由其实施。
您可以使用 KeyInfo.isUserAuthenticationRequirementEnforcedBySecureHardware() 查询密钥的用户身份验证授权是否由安全硬件实施。
选择密钥链或 Android 密钥库提供程序
在需要系统级凭据时请使用 KeyChain API。在应用通过 KeyChain API 请求使用任何凭据时,用户需要通过系统提供的 UI 选择应用可以访问已安装的哪些凭据。因此,在用户同意的情况下多个应用可以使用同一套凭据。
使用 Android 密钥库提供程序让各个应用存储自己的凭据,并且只允许应用自身访问。这样,应用可以管理仅能由自己使用的凭据,同时又可以提供等同于 KeyChain API 为系统级凭据提供的安全优势。这一方法不需要用户选择凭据。
使用 Android 密钥库提供程序
要使用此功能,请使用标准的 KeyStore 和 KeyPairGenerator 或 KeyGenerator 类,以及在 Android 4.3(API 级别 18)中引入的 AndroidKeyStore 提供程序。
AndroidKeyStore 注册为 KeyStore 类型以用于 KeyStore.getInstance(type) 方法,而在用于 KeyPairGenerator.getInstance(algorithm, provider) 和 KeyGenerator.getInstance(algorithm, provider) 方法时则注册为提供程序。
生成新私钥
生成新的 PrivateKey 要求您同时指定自签署证书具备的初始 X.509 属性。之后,您可以使用 KeyStore.setKeyEntry 将证书替换为由证书颁发机构 (CA) 签署的证书。
要生成密钥,则使用 KeyPairGenerator 和 KeyPairGeneratorSpec:
/*
* Generate a new EC key pair entry in the Android Keystore by
* using the KeyPairGenerator API. The private key can only be
* used for signing or verification and only with SHA-256 or
* SHA-512 as the message digest.
*/
KeyPairGenerator kpg = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
kpg.initialize(new KeyGenParameterSpec.Builder(
alias,
KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
.setDigests(KeyProperties.DIGEST_SHA256,
KeyProperties.DIGEST_SHA512)
.build());
KeyPair kp = kpg.generateKeyPair();
生成新密钥
要生成密钥,请使用 KeyGenerator 和 KeyGenParameterSpec。
使用密钥库条目
AndroidKeyStore 提供程序的使用通过所有的标准 KeyStore API 加以实现。
列出条目
通过调用 aliases() 方法列出密钥库中的条目:
/*
* Load the Android KeyStore instance using the the
* "AndroidKeyStore" provider to list out what entries are
* currently stored.
*/
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
Enumeration<String> aliases = ks.aliases();
签署和验证数据
通过从密钥库提取 KeyStore.Entry 并使用 Signature API(例如 sign())签署数据:
/*
* Use a PrivateKey in the KeyStore to create a signature over
* some data.
*/
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
KeyStore.Entry entry = ks.getEntry(alias, null);
if (!(entry instanceof PrivateKeyEntry)) {
Log.w(TAG, "Not an instance of a PrivateKeyEntry");
return null;
}
Signature s = Signature.getInstance("SHA256withECDSA");
s.initSign(((PrivateKeyEntry) entry).getPrivateKey());
s.update(data);
byte[] signature = s.sign();
类似地,请使用 verify(byte[]) 方法验证数据:
/*
* Verify a signature previously made by a PrivateKey in our
* KeyStore. This uses the X.509 certificate attached to our
* private key in the KeyStore to validate a previously
* generated signature.
*/
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
KeyStore.Entry entry = ks.getEntry(alias, null);
if (!(entry instanceof PrivateKeyEntry)) {
Log.w(TAG, "Not an instance of a PrivateKeyEntry");
return false;
}
Signature s = Signature.getInstance("SHA256withECDSA");
s.initVerify(((PrivateKeyEntry) entry).getCertificate());
s.update(data);
boolean valid = s.verify(signature);
要求进行用户身份验证才能使用密钥
生成密钥或将密钥导入到 AndroidKeyStore 时,您可以指定密钥仅授权给经过身份验证的用户使用。用户使用安全锁定屏幕凭据(模式/PIN/密码、指纹)的子集进行身份验证。
这是一项高级安全功能,通常仅用于有以下要求的情形:在生成/导入密钥后(而不是之前或当中),应用进程受到攻击不会导致密钥被未经身份验证的用户使用。
如果密钥仅授权给经过身份验证的用户使用,可以将其配置为以下列两种模式之一运行:
- 经过身份验证的用户可以在一段时间内使用密钥。在用户解锁安全锁定屏幕或使用 KeyguardManager.createConfirmDeviceCredentialIntent 流程确认其安全锁定屏幕凭据后,即可使用此模式中的所有密钥。每个密钥的授权持续时间各不相同,并由 setUserAuthenticationValidityDurationSeconds 在密钥生成或导入时指定。此类密钥只能在启用安全锁定屏幕时生成或导入(请参阅 KeyguardManager.isDeviceSecure())。在安全锁定屏幕停用(重新配置为“无”、“滑动”或不验证用户身份的其他模式)或被强制重置(例如由设备管理员执行)时,这些密钥将永久失效。
- 用户身份验证会授权与某一密钥关联的特定加密操作。在此模式中,涉及此类密钥的每个操作都需要用户单独授权。目前,此类授权的唯一方式是指纹身份验证:FingerprintManager.authenticate。此类密钥只能在至少注册一个指纹时生成或导入(请参阅 FingerprintManager.hasEnrolledFingerprints)。一旦注册新指纹或取消注册所有指纹,这些密钥将永久失效。
Android数据加密:
Android 提供了 KeyStore 等可以长期存储和检索加密密钥的机制,Android KeyStore 系统特别适合于存储加密密钥。
“AndroidKeyStore” 是 KeyStore 的一个子集,存进 AndroidKeyStore 的 key 将受到签名保护,并且这些 key 是存在系统里的,而不是在 App 的 data 目录下,依托于硬件的 KeyChain 存储,可以做到 private key 一旦存入就无法取出,
每个 App 自己创建的 key,别的应用是访问不到的。
它提供了限制何时以何种方式使用密钥的方法,比如使用密钥时需要用户认证或限制密钥只能在加密模式下使用。
一个应用程式只能编辑、保存、取出自己的密钥。
App可以生成或者接收一个公私密钥对,并存储在Android的Keystore系统中。公钥可以用于在应用数据放置到特定文件夹前对数据进行加密,私钥可以在需要的时候解密相应的数据。
作用:
KeyStore 适用于生成和存储密钥,这些密钥可以用来加密运行时获取到的数据,比如运行时,用户输入的密码,或者服务端传下来的 token。
操作方式
建议做法
1. 使用对称式加解密,但只能在Api Level 23+使用
对称式加解密(AES)速度较快,但是对称式的Key若要存在 KeyStore 裡,Api Level一定要在23以上才支持,23以下是无法存入 KeyStore 的,非对称式的Key則不在此限。
2. 想兼容各Api版本(23以下也能用)
- 若要存取的東西不多、字串長度也不長:直接使用非对称式加解密即可
- 若要存取的東西很多或字串長度很長:由於非对称式加解密速度较慢,使用非对称式+对称式加解密可以解決此問題。
考慮到加解密效能、版本兼容,下面會介紹用非对称式+对称式來加解密。
KeyStore非对称+对称式加解密流程
- 使用KeyStore产生随机的RSA Key;
- 产生AES Key,并用RSA Public Key加密后存入SharedPrefs;
- 从SharedPrefs取出AES Key,並用RSA Private Key解密,用這把AES Key來加解密信息;
主流的加密方式有:(对称加密)AES、DES (非对称加密)RSA、DSA
工作模式:
DES一共有:
电子密码本模式(ECB)、加密分组链接模式(CBC)、加密反馈模式(CFB)、输出反馈模式(OFB);
AES一共有:
电子密码本模式(ECB)、加密分组链接模式(CBC)、加密反馈模式(CFB)、输出反馈模式(OFB)、计数器模式(CTR),伽罗瓦计数器模式(GCM)
PKCS5Padding是填充模式,还有其它的填充模式;
对于初始化向量iv: 初始化向量参数,AES 为16bytes. DES 为8bytes
private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
// 算法/模式/补码方式 不使用CBC模式,注意Padding Oracle攻击
private static final String AES_MODE = "AES/GCM/NoPadding";
/*
* RSA ENCRYPTION_PADDING_RSA_PKCS1 存在安全漏洞,高于API 23的版本使用 ENCRYPTION_PADDING_RSA_OAEP
* 漏洞关键字:
* "Chosen ciphertext attacks against protocols based on the RSA encryption standard PKCS #1"
* 目前测试来看,很多设备暂时不支持 ENCRYPTION_PADDING_RSA_OAEP 模式,比较可惜
*/
private static final String RSA_MODE = "RSA/ECB/PKCS1Padding";
private static final String KEYSTORE_ALIAS = "KEYSTORE_DEMO";
/**
* Returns whether the device has a StrongBox backed KeyStore.
*/
public static boolean hasStrongBox(@NonNull final Context context) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return context.getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE);
}
return false;
}
KeyStore mKeyStore = KeyStore.getInstance(KEYSTORE_PROVIDER);
mKeyStore.load(null);
(1)产生随机的 RSA Key
产生 RSA Key 会使用到 KeyPairGenerator:
其中 KeyPairGeneratorSpec 在Api 23以上已經Deprecated了;
Api level 23以上改使用 KeyGenParameterSpec
/*
* 注意KeyStore的API线程不安全,不建议主线程执行,主线程可能导致界面卡顿
* 建议全部调度到一个统一的HanderThread子线程进行处理
**/
@WorkThread
private void genKeyStoreKey(@NonNull final Context context) throws Exception {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
generateRSAKey_AboveApi23(context);
} else {
generateRSAKey_BelowApi23(context);
}
}
Api 23 以上使用 KeyGenParameterSpec
/*
* 注意KeyStore的API线程不安全,不建议主线程执行,主线程可能导致界面卡顿
* 建议全部调度到一个统一的HanderThread子线程进行处理
**/
@WorkThread
@RequiresApi(api = Build.VERSION_CODES.M)
private void generateRSAKey_AboveApi23(@NonNull final Context context) throws Exception {
final KeyPairGenerator keyPairGenerator = KeyPairGenerator
.getInstance(KeyProperties.KEY_ALGORITHM_RSA, KEYSTORE_PROVIDER);
final Calendar end = Calendar.getInstance();
end.add(Calendar.YEAR, 99);
final KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec
.Builder(KEYSTORE_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
/*
* RSA ENCRYPTION_PADDING_RSA_PKCS1 存在安全漏洞,高于API 23的版本使用 ENCRYPTION_PADDING_RSA_OAEP
* 漏洞关键字:
* "Chosen ciphertext attacks against protocols based on the RSA encryption standard PKCS #1"
* 目前测试来看,很多设备暂时不支持 ENCRYPTION_PADDING_RSA_OAEP 模式,比较可惜
*/
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
.setBlockModes(KeyProperties.BLOCK_MODE_ECB)
/* 开始时间设置为1970/1/1原因在于
* 防止系统由于时间无效或者电池没电导致的时间只能从最早时间开始的问题,
* 如果时间不正确会导致在调用的时候抛出
* android.security.keystore.KeyNotYetValidException: Key not yet valid
*/
.setKeyValidityStart(new Date(0))
.setKeyValidityForConsumptionEnd(end.getTime())
.setKeyValidityForOriginationEnd(end.getTime())
.setCertificateSubject(new X500Principal("CN=" + KEYSTORE_ALIAS));
/* 如果系统支持硬件StrongBox保存密钥,则交给StrongBox保护*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
builder.setIsStrongBoxBacked(hasStrongBox(context));
}
final KeyGenParameterSpec spec = builder.build();
keyPairGenerator.initialize(spec);
keyPairGenerator.generateKeyPair();
}
Api 23 以下使用 KeyPairGeneratorSpec
/*
* 注意KeyStore的API线程不安全,不建议主线程执行,主线程可能导致界面卡顿
* 建议全部调度到一个统一的HanderThread子线程进行处理
**/
@WorkThread
private void generateRSAKey_BelowApi23(@NonNull final Context context) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
final Calendar end = Calendar.getInstance();
end.add(Calendar.YEAR, 99);
final KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context)
.setAlias(KEYSTORE_ALIAS)
.setSubject(new X500Principal("CN=" + KEYSTORE_ALIAS))
.setSerialNumber(BigInteger.TEN)
/* 开始时间设置为1970/1/1原因在于
* 防止系统由于时间无效或者电池没电导致的时间只能从最早时间开始的问题,
* 如果时间不正确会导致在调用的时候抛出
* android.security.keystore.KeyNotYetValidException: Key not yet valid
*/
.setStartDate(new Date(0))
.setEndDate(end.getTime())
.build();
final KeyPairGenerator keyPairGenerator = KeyPairGenerator
.getInstance("RSA", KEYSTORE_PROVIDER);
keyPairGenerator.initialize(spec);
keyPairGenerator.generateKeyPair();
}
注意,已知,在某些低端设备上,RSA密钥对生成的时间可能超过1S以上。尝试过异步子线程初始化,但是由KeyStore内部的函数在实现的时候,线程不安全,导致子线程初始化的时候诱发了异常行为,因此只能在主线程中进行操作。
(2)产生AES Key后,并用RSA Public Key加密后存入SharedPrefs
private void genAESKey() throws Exception {
// Generate AES-Key
final byte[] aesKey = new byte[16];
final SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(aesKey);
// Generate 12 bytes iv then save to SharedPrefs
final byte[] generated = secureRandom.generateSeed(12);
final String iv = Base64.encodeToString(generated, Base64.DEFAULT);
final String encryptIV = encryptRSA(iv.getBytes());
prefsHelper.setIV(encryptIV);
// Encrypt AES-Key with RSA Public Key then save to SharedPrefs
final String encryptAESKey = encryptRSA(aesKey);
prefsHelper.setAESKey(encryptAESKey);
}
1]加密存储:使用RSA Public Key 加密 AES Key,存入缓存中。
2] 解密使用:使用RSA Private Key 解密 得到 AES Key。
private String encryptRSA(@NonNull final byte[] plainText) throws Exception {
final PublicKey publicKey = keyStore.getCertificate(KEYSTORE_ALIAS).getPublicKey();
final Cipher cipher = Cipher.getInstance(RSA_MODE);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
final byte[] encryptedByte = cipher.doFinal(plainText);
return Base64.encodeToString(encryptedByte, Base64.DEFAULT);
}
private byte[] decryptRSA(@NonNull final String encryptedText) throws Exception {
final PrivateKey privateKey = (PrivateKey) keyStore.getKey(KEYSTORE_ALIAS, null);
final Cipher cipher = Cipher.getInstance(RSA_MODE);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
final byte[] encryptedBytes = Base64.decode(encryptedText, Base64.DEFAULT);
final byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
return decryptedBytes;
}
获取AES :
private SecretKeySpec getAESKey() throws Exception {
final String encryptedKey = prefsHelper.getAESKey();
final byte[] aesKey = decryptRSA(encryptedKey);
return new SecretKeySpec(aesKey, AES_MODE);
}
再使用AES 加解密内容:
对于:Cipher 初始化
//实例化加密类,参数为加密方式,要写全
// 算法/模式/补码方式 不使用CBC模式,注意Padding Oracle攻击
Cipher cipher = Cipher.getIntance("AES/CBC/PKCS5Padding");
//初始化,此方法可以采用三种方式,按服务器要求来添加。
//(1)无第三个参数
//(2)第三个参数为SecureRandom random = new SecureRandom();
// 中random对象,随机数。(AES不可采用这种方法)
//(3)第三个参数:IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec/random);
具体使用:
/**
* AES Encryption
* @param plainText: A string which needs to be encrypted.
* @return A base64's string after encrypting.
*/
private String encryptAES(@NonNull final String plainText) throws Exception {
final Cipher cipher = Cipher.getInstance(AES_MODE);
cipher.init(Cipher.ENCRYPT_MODE, getAESKey(), new IvParameterSpec(getIV()));
// 加密過後的byte
final byte[] encryptedBytes = cipher.doFinal(plainText.getBytes());
// 將byte轉為base64的string編碼
return Base64.encodeToString(encryptedBytes, Base64.DEFAULT);
}
private String decryptAES(@NonNull final String encryptedText) throws Exception {
// 將加密過後的Base64編碼格式 解碼成 byte
final byte[] decodedBytes = Base64.decode(encryptedText.getBytes(), Base64.DEFAULT);
// 將解碼過後的byte 使用AES解密
final Cipher cipher = Cipher.getInstance(AES_MODE);
cipher.init(Cipher.DECRYPT_MODE, getAESKey(), new IvParameterSpec(getIV()));
return new String(cipher.doFinal(decodedBytes));
}
iv 初始化向量
private byte[] getIV() throws Exception {
final String prefIV = prefsHelper.getIV();
final byte[] iv = decryptRSA(prefIV);
return Base64.decode(iv, Base64.DEFAULT);
}
关于RSA:
使用RSA加解密时,在较低版本的手机上可能无法选择OAEP(最优非对称加密填充,RSA的加密解密是基于OAEP的)這個模式;
因此可以改使用RSA_PKCS1_PADDING模式,使用这个模式的話,输入必须比RSA的Key最大长度少11個字节,如果需要被加密的字串过长的话,可以在产生Key时指定Key Size长度,或是将字串分段加密。
以预设Key Size = 2048bit(256byte)來说,输入最长只能到256–11=245byte,我們可以透过setKeySize(int keySize)指定Key的长度,但是Key Size越大,加解密时速度就越慢。
KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec
.Builder(KEYSTORE_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
.setKeySize(4096)
.build();
需要注意,由于设备可能存储密钥到硬件设备(KeyInfo.isInsideSecurityHardware()),然而硬件设备不一定能保存我们手工指定的某些长度的密钥。导致如果我们设置了指定长度,可能由于硬件设备不支持,反而只能存储到系统中,造成密钥存储的安全性反而下降了。
我们希望厂家设置的默认值是硬件能支持的最大安全性,并且尽量存储到硬件中。尽管密钥安全性可能下降了,但是存储安全性反而上升了。
或者从高到低重试,选择当前设备支持的最高的硬件存储的长度。究竟是存储位置重要还是加密级别更重要,需要权衡。
判断生成的密钥是否由硬件存储,参考如下代码:
/*API 23 以下的硬件,我们默认就无法存储密钥到硬件设备*/
public boolean isInsideSecurityHardware() throws Exception {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
final PrivateKey key = (PrivateKey) keyStore.getKey(KEYSTORE_ALIAS, null);
final KeyFactory factory = KeyFactory.getInstance(key.getAlgorithm());
final KeyInfo info = factory.getKeySpec(key, KeyInfo.class);
//RSA密钥的长度也可以通过KeyInfo进行查询
return info.isInsideSecurityHardware();
}
return false;
}
获取设备生成的密钥的长度,参考如下代码:
public int getSecureKeySize() throws Exception {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
final PrivateKey key = (PrivateKey) keyStore.getKey(KEYSTORE_ALIAS, null);
final KeyFactory factory = KeyFactory.getInstance(key.getAlgorithm());
final KeyInfo info = factory.getKeySpec(key, KeyInfo.class);
if (null != info) {
return info.getKeySize();
}
} else {
final Certificate certificate = keyStore.getCertificate(KEYSTORE_ALIAS);
if (certificate instanceof X509Certificate) {
final X509Certificate cert = (X509Certificate) certificate;
final PublicKey publicKey = cert.getPublicKey();
if (publicKey instanceof RSAPublicKey) {
final RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey;
final BigInteger bigInteger = rsaPublicKey.getModulus();
if (null != bigInteger) {
return bigInteger.bitLength();
}
}
}
}
return 0;
}
需要解决的一个疑惑就是,既然可以通过
final PrivateKey privateKey = (PrivateKey) keyStore.getKey(KEYSTORE_ALIAS, null);
的方式获得RSA私钥,那么,我们能不导出这个私钥呢?
答案显然是不能的,原因在于系统给出的私钥只是一个代理,并没有实际的私钥数据,私钥数据被存储在相关的硬件或者系统内核中,主要证据就是`privateKey.getEncoded()`返回了`null`,这样就实现了关键的私钥数据都无法获得。
参考如下测试代码:
final PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, null);
assertNotNull(privateKey);
// 默认的AndroidKeyStore应该不能读出私钥数据
assertNull(privateKey.getEncoded());
assertNull(privateKey.getFormat());
/* RSA 密钥重新生产的方法参考下面的代码,从获取到的 PrivateKey中我们只能得到系数(modulus),其他参数都得不到
因此保证了密钥的安全性
RSAPrivateCrtKeySpec spec = new RSAPrivateCrtKeySpec(modulus, publicExponent, privateExponent, primeP, primeQ, primeExponentP, primeExponentQ, crtCoefficient);
KeyFactory keyFactory = KeyFactory.getInstance("RSA","BC");
PrivateKey privateKey = keyFactory.generatePrivate(spec);
*/
//公钥要可以被导出
final Certificate cert = keyStore.getCertificate(alias);
assertNotNull(cert.getEncoded());
需要注意的一个问题在于,由于RSA的大素数搜索机制,导致每次生成密钥的时间可能会超过预期,最长的可能会耗时1-2S以上。因此,如果设备支持,我们直接申请硬件AES密钥的方式来进行数据的加解密操作,达到更高的安全程度。
可用的参考代码如下:
import android.content.Context;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyInfo;
import android.security.keystore.KeyProperties;
import android.util.Base64;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import org.json.JSONObject;
import java.security.KeyStore;
import java.security.spec.KeySpec;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
/**
* 通过AES加密方式,用KeyGenerator生成秘钥,保存在Android Keystore中
* 对数据进行加解密
* <p>
* 1、创建秘钥,保存在AndroidKeystore里面,秘钥别名为alias
* 2、创建并初始化cipher对象,获取秘钥,对数据进行加解密
*/
class HwAesSecProvider {
private static final String TAG = "HwAesSecProvider";
private static final String KEYSTORE_AES_ALIAS = "Alias_AES_";
// 算法/模式/补码方式 不使用CBC模式,注意Padding Oracle攻击
private static final String AES_MODE = "AES/GCM/NoPadding";
private final static String KEY_IV = "iv";
private final static String KEY_DATA = "data";
private final Context context;
private final KeyStore keyStore;
public HwAesSecProvider(@NonNull final Context context, @NonNull final KeyStore keyStore) {
this.context = context;
this.keyStore = keyStore;
}
@NonNull
private static String getKeyStoreAlias(@NonNull final Context context) {
return KEYSTORE_AES_ALIAS + PrefsHelper.getPackageNameHash(context);
}
@NonNull
public String decryptAES(@NonNull final String encryptedText) {
try {
final JSONObject json = new JSONObject(encryptedText);
final byte[] iv = Base64.decode(json.optString(KEY_IV, ""), Base64.DEFAULT);
final byte[] data = Base64.decode(json.optString(KEY_DATA, ""), Base64.DEFAULT);
if (iv.length <= 0 || data.length <= 0) {
return "";
}
final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry(getKeyStoreAlias(context), null);
if (null != secretKeyEntry) {
final SecretKey secretKey = secretKeyEntry.getSecretKey();
//KeyGenParameterSpecs中设置的block模式是KeyProperties.BLOCK_MODE_GCM,所以这里只能使用这个模式解密数据。
final Cipher cipher = Cipher.getInstance(AES_MODE);
//需要为GCMParameterSpec 指定一个加密的block的长度(可以是128、120、112、104、96),从密钥信息中读取后填入
// 并且用到之前的加密过程中用到的IV。
final GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(getSecureKeySize(secretKey), iv, 0, iv.length);
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec);
return new String(cipher.doFinal(data));
}
} catch (Throwable t) {
Logs.e(TAG, t.getMessage(), t);
}
return "";
}
@NonNull
public String encryptAES(@NonNull final String plainText) {
try {
final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry(getKeyStoreAlias(context), null);
if (null != secretKeyEntry) {
final SecretKey secretKey = secretKeyEntry.getSecretKey();
//KeyGenParameterSpecs中设置的block模式是KeyProperties.BLOCK_MODE_GCM,所以这里只能使用这个模式解密数据。
final Cipher cipher = Cipher.getInstance(AES_MODE);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
final JSONObject json = new JSONObject();
json.put(KEY_DATA, Base64.encodeToString(cipher.doFinal(plainText.getBytes()), Base64.DEFAULT));
/*使用相同的IV多次加密不同的数据,会存在被恶意穷举的风险,我们要求硬件每次加密返回不同的加密向量IV
*这就要求我们需要存储加密数据的同时,存储返回的随机向量,然后在解密的时候提供加密时候的随机向量
*/
json.put(KEY_IV, Base64.encodeToString(cipher.getIV(), Base64.DEFAULT));
return json.toString();
}
} catch (Throwable t) {
Logs.e(TAG, t.getMessage(), t);
}
return "";
}
/*
* 注意KeyStore的API线程不安全,不建议主线程执行,主线程可能导致界面卡顿
* 建议全部调度到一个统一的HanderThread子线程进行处理
**/
@WorkThread
@RequiresApi(api = Build.VERSION_CODES.M)
private void generateHwAesKey_AboveApi23(@NonNull final String keystoreAlias, @NonNull final String provider) throws Exception {
final KeyGenerator keyGenerator = KeyGenerator
.getInstance(KeyProperties.KEY_ALGORITHM_AES, provider);
// https://developer.android.com/training/articles/keystore.html#SupportedCiphers)
// https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder.html#setRandomizedEncryptionRequired(boolean)).
final KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec
.Builder(keystoreAlias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setCertificateSubject(new X500Principal("CN=" + getKeyStoreAlias(context)))
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
/*使用相同的IV多次加密不同的数据,会存在被恶意穷举的风险,我们要求硬件每次加密返回不同的加密向量IV
*这就要求我们需要存储加密数据的同时,存储返回的随机向量,然后在解密的时候提供加密时候的随机向量
*/
.setRandomizedEncryptionRequired(true); //要求硬件生成随机向量
/* 如果系统支持硬件StrongBox保存密钥,则交给StrongBox保护*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
builder.setIsStrongBoxBacked(hasStrongBox(context));
}
final KeyGenParameterSpec spec = builder.build();
keyGenerator.init(spec);
keyGenerator.generateKey();
}
/*
* 注意KeyStore的API线程不安全,不建议主线程执行,主线程可能导致界面卡顿
* 建议全部调度到一个统一的HanderThread子线程进行处理
**/
@WorkThread
public boolean generateSecStoreKeys() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
generateHwAesKey_AboveApi23(getKeyStoreAlias(context), SecConst.AND_KEYSTORE_PROVIDER);
return isInsideSecureHardware();
} catch (Throwable t) {
Logs.e(TAG, t.getMessage(), t);
}
}
return false;
}
public boolean isInsideSecureHardware() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
final KeyInfo info = getKeyInfo();
if (null != info) {
return info.isInsideSecureHardware();
}
}
return false;
}
public int getSecureKeySize() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
final KeyInfo info = getKeyInfo();
if (null != info) {
return info.getKeySize();
}
}
return 0;
}
@RequiresApi(api = Build.VERSION_CODES.M)
@Nullable
private KeyInfo getKeyInfo(@NonNull final SecretKey secretKey) throws Exception {
final SecretKeyFactory factory = SecretKeyFactory.getInstance(secretKey.getAlgorithm(), SecConst.AND_KEYSTORE_PROVIDER);
if (null != factory) {
final KeySpec spec = factory.getKeySpec(secretKey, KeyInfo.class);
if (null != spec) {
return (KeyInfo) spec;
}
}
return null;
}
private int getSecureKeySize(@NonNull final SecretKey secretKey) throws Exception {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
final KeyInfo info = getKeyInfo(secretKey);
if (null != info) {
return info.getKeySize();
}
}
return 0;
}
@RequiresApi(api = Build.VERSION_CODES.M)
@Nullable
private KeyInfo getKeyInfo(@NonNull final String alias) {
try {
final SecretKey key = (SecretKey) keyStore.getKey(alias, null);
if (null != key) {
return getKeyInfo(key);
}
} catch (Throwable t) {
Logs.e(TAG, "Exception getting key info", t);
}
return null;
}
@RequiresApi(api = Build.VERSION_CODES.M)
@Nullable
private KeyInfo getKeyInfo() {
return getKeyInfo(getKeyStoreAlias(context));
}
/**
* Returns whether the device has a StrongBox backed KeyStore.
*/
public static boolean hasStrongBox(@NonNull final Context context) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return context.getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE);
}
return false;
}
}
参考链接