AndroidKeystore密钥库系统,保证本地加密算法的密钥安全性

1、AndroidKeystore密钥库系统介绍

AndroidKeystore系统是一个密钥库管理系统,谷歌设计这个系统的初衷应该是为了对标苹果的钥匙串KeyChain,有意思的是谷歌在Android4.0(API14)时便引入了KeyChain,但是并未提供详尽的说明文档,仅仅提供了一个API文档,网上也没有什么对它的文章,着实让人费解。话不多说,有兴趣的同学可以自行搜索,下面我来给大家介绍一下今天的主角——AndroidKeystore

谷歌在Android4.3(API18)中引入了AndroidKeystore,在Android6.0(API23)中对AndroidKeystore进行了一波升级优化,目前来说应该是最安全的加解密方案。根据官方文档说明,原文对它的介绍如下:

利用 Android Keystore 系统,您可以在容器中存储加密密钥,从而提高从设备中提取密钥的难度。在密钥进入 Keystore 后,可以将它们用于加密操作,而密钥材料仍不可导出。此外,它提供了密钥使用的时间和方式限制措施,例如要求进行用户身份验证才能使用密钥,或者限制为只能在某些加密模式中使用。

乍一看,这似乎是一个密钥保险箱,安全性高,可以把加密密钥存入其中,需要的时候再通过它用我们的密钥。可是!!这货的使用根本就不是个存储空间!!我们先来看看这东西大致的使用流程是怎样的,让大家直观的明白它的作用:

// step 1  通过密钥别名判断是否已有密钥
keyStore.containsAlias(keyAlias);

// step 2  没有密钥,生成一个
spec = new KeyGenParameterSpec.Builder(keyAlias, ...)...;
keyPairGenerator.initialize(spec);
keyPairGenerator.generateKeyPair();

// step 3  取出公钥,进行加密
publicKey = ((KeyStore.PrivateKeyEntry)entry).getCertificate().getPublicKey();
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
cipher.doFinal(data);

// step 4  取出私钥,进行解密
privateKey = ((KeyStore.PrivateKeyEntry)entry).getPrivateKey();
cipher.init(Cipher.DECRYPT_MODE, privateKey);
cipher.doFinal(data);

如此看来,AndroidKeystore根本就不是个密钥存储库。这和EncryptUtil这样的工具类几乎是一样的,先init,再encrypt,然后decrypt,俨然一个加解密工具。说白了,它在内部生成一个密钥,我们可以用它来加解密数据,并不能把我们自己生成的密钥送进去。

根据文档的介绍,它的大部分功能都只在Android6.0(API23)及以上支持,最低支持的Android4.3(API18)只提供RSA/ECB算法的使用。

2、密钥的检测与生成

AndroidKeystore是通过密钥的别名来进行加解密的,那么我们在使用之前必然是需要判断一下密钥是否存在的,AndroidKeystore提供了containsAlias()方法来完成密钥的检测,具体使用如下:

public boolean hasKey(String keyAlias) {
	try {
		// 加载一个AndroidKeyStore类型的KeyStore,貌似是定死的类型。
		KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
		keyStore.load(null);
		return keyStore.containsAlias(keyAlias);
	} catch (KeyStoreException e) {
		e.printStackTrace();
	} catch (CertificateException e) {
		e.printStackTrace();
	} catch (NoSuchAlgorithmException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	}
	return false;
}

这里可以"AndroidKeyStore"用常量存一下,后面还会用到,避免魔法字符串。关于字符串常量我就不单独提出来了,不方便阅读,接下来有使用的字符串都建议在项目中提取为字符串常量。

密钥的检测很简单,我就不赘述了,这里不得不吐槽一句,异常真多,当然后面也是一样的多异常,总让人用起来不踏实的感觉。

如果密钥库中没有该别名的密钥,那么我们得让它生成一个,重头戏来了,敲黑板!!!

public void buildKey(Context context, String keyAlias) {
	try{
		// 先获取密钥对生成器,采用RSA算法,AndroidKeyStore类型
		KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
		// 加密算法的相关参数
		AlgorithmParameterSpec spec;
		// 密钥的有效起止时间,从现在到999年后,时间大家自己定
		Calendar start = Calendar.getInstance();
		Calendar end = Calendar.getInstance();
		end.add(Calendar.YEAR, 999)
		// 生成加密参数,从Android6.0(API23)开始有所不同
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
			// 根据密钥别名生成加密参数,提供加密和解密操作
			spec = new KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
				// SHA (Secure Hash Algorithm,译作安全散列算法) 是美国国家安全局 (NSA) 设计,美国国家标准与技术研究院 (NIST) 发布的一系列密码散列函数。 感兴趣的同学可以了解一下
				.setDigests(KeyProperties.DIGEST_SHA512)
				// 填充模式,一般RSA加密常用PKCS1Padding模式
				.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
				// 限定密钥有效期起止时间
				.setCertificateNotBefore(start.getTime())
				.setCertificateNotAfter(end.getTime())
				.build();
		} else {
			// 相对于Android6.0(API23)的方式,这种稍显简单
			spec = new KeyPairGeneratorSpec.Builder(context.getApplicationContext())
				.setAlias(keyAlias)
				// 设置用于生成的密钥对的自签名证书的主题,X500Principal这东西不认识,资料真少,看的头大
				.setSubject(new X500Principal("CN=" + keyAlias))
				// 设置用于生成的密钥对的自签名证书的序列号,从BigInteger取即可
				.setSerialNumber(BigInteger.TEN)
				// 限定密钥有效期起止时间
				.setStartDate(start.getTime())
				.setEndDate(end.getTime())
				.build()
		}
		// 用加密参数初始化密钥对生成器,生成密钥对
		keyPairGenerator.initialize(spec);
		keyPairGenerator.generateKeyPair();
	} catch (NoSuchAlgorithmException e) {
		e.printStackTrace();
	} catch (NoSuchProviderException e) {
		e.printStackTrace();
	} catch (InvalidAlgorithmParameterException e) {
		e.printStackTrace();
	}
}

话不多说,都在代码注释里了,有没讲清楚的地方,欢迎评论区指出。

3、加解密流程

来吧,进入令人颤抖的加解密流程,真的,当我写完我都不可思议。

加解密的流程非常相似,先加载KeyStore,再拿到公钥/私钥,最后进行加密/解密。代码实现也基本一致,那么问题来了,令人颤抖的是什么呢?亮代码:

// 加密方法
public byte[] encrypt(Context context, String keyAlias, byte[] data) {
	if(!hasKey(keyAlias)) {
		buildKey(context, keyAlias);
	}
	try {
		// 获取"AndroidKeyStore"类型的KeyStore,加载
		KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
		keyStore.load(null);
		// 拿到密钥别名对应的Entry
		KeyStore.Entry entry = keyStore.getEntry(keyAlias, null);
		if (entry instanceof KeyStore.PrivateKeyEntry) {
			// 通过Entry拿到公钥对象(并不是真实的公钥,仅供加密方法使用)
			PublicKey publicKey = ((KeyStore.PrivateKeyEntry)entry).getCertificate().getPublicKey();
			// 使用"RSA/ECB/PKCS1Padding"模式进行加密
			Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
			cipher.init(Cipher.ENCRYPT_MODE, publicKey);
			return cipher.doFinal(data);
		}
	} catch (IOException e) {
		e.printStackTrace();
	} catch (CertificateException e) {
		e.printStackTrace();
	} catch (NoSuchAlgorithmException e) {
		e.printStackTrace();
	} catch (UnrecoverableEntryException e) {
		e.printStackTrace();
	} catch (KeyStoreException e) {
		e.printStackTrace();
	} catch (NoSuchPaddingException e) {
		e.printStackTrace();
	} catch (InvalidKeyException e) {
		e.printStackTrace();
	} catch (BadPaddingException e) {
		e.printStackTrace();
	} catch (IllegalBlockSizeException e) {
		e.printStackTrace();
	}
	return null;
}


// 解密方法
public byte[] decrypt(Context context, String keyAlias, byte[] data) {
	if(!hasKey(keyAlias)) {
		buildKey(context, keyAlias);
	}
	try {
		// 获取"AndroidKeyStore"类型的KeyStore,加载
		KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
		keyStore.load(null);
		// 拿到密钥别名对应的Entry
		KeyStore.Entry entry = keyStore.getEntry(keyAlias, null);
		if (entry instanceof KeyStore.PrivateKeyEntry) {
			// 通过Entry拿到私钥对象(并不是真实的私钥,仅供解密方法使用)
			PrivateKey privateKey = ((KeyStore.PrivateKeyEntry)entry).getPrivateKey();
			// 使用"RSA/ECB/PKCS1Padding"模式进行解密
			Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
			cipher.init(Cipher.DECRYPT_MODE, privateKey);
			return cipher.doFinal(data);
		}
	} catch (IOException e) {
		e.printStackTrace();
	} catch (CertificateException e) {
		e.printStackTrace();
	} catch (NoSuchAlgorithmException e) {
		e.printStackTrace();
	} catch (UnrecoverableEntryException e) {
		e.printStackTrace();
	} catch (KeyStoreException e) {
		e.printStackTrace();
	} catch (NoSuchPaddingException e) {
		e.printStackTrace();
	} catch (InvalidKeyException e) {
		e.printStackTrace();
	} catch (BadPaddingException e) {
		e.printStackTrace();
	} catch (IllegalBlockSizeException e) {
		e.printStackTrace();
	}
	return null;
}

谜题揭晓,令人颤抖的异常!!这是我至今为止,捕获的最多的异常了,果真是头发短见识短,疫情让我的头发长长了,见识的增长也接踵而来。

4、常规用法

通常只把AndroidKeystore用于对密钥的加密,也就是说,建议将需要加密的数据使用对称加密算法(如AES)进行加密,而对称加密算法的密钥则由AndroidKeystore进行加密保护。通常用法如下:

// saveKey()方法用于将加密后的aesKey持久化,parseByte2Hex()方法用于将二进制转为十六进制,防止byte[]转String导致格式出错的问题。 
saveKey(parseByte2Hex(encrypt(context, keyAlias, aesKey)));

// readKey()方法用于读取持久化的加密的aesKey,parseHex2Byte()方法用于将十六进制转为二进制,防止String转byte[]导致格式出错的问题。 
aesKey = decrypt(context, key, parseHex2Byte(readKey()));
posted @ 2020-11-11 19:40  coder-ice  阅读(3219)  评论(0编辑  收藏  举报