参考文档:
http://developers.googleblog.cn/2017/10/android.html
https://developer.android.com/training/articles/keystore.html#SecurityFeatures
https://developer.android.google.cn/reference/javax/crypto/KeyGenerator.html
https://developer.android.google.cn/reference/java/security/KeyStore.html
1. 前言
学习这段的时候,我也是云里雾里,不知道这玩意有啥用。一般来说,安全登录机制,主要依靠服务端来实现,客户端保存一个cookie或token就完事了。
参考文档1介绍说,对于敏感操作,需要用户经常输入密码,例如微信支付宝支付时需要输入支付密码。输入密码不方便,经常输错,虽然微信支付宝已经把支付密码改的很简单了。
本人对安全机制并不是专家,如果下面的内容有什么问题,请指正。如果实践中有什么问题,请自行负责。哈哈哈。
使用安卓密钥库,就可以不再输入密码了。只需要用户输入复杂密码一次,生成一个非对称的身份验证密钥(应该即私钥),在银行的客户账户数据库中注册公钥。以后用户就拿这个私钥去后台验证身份即可。
在最新的系统中,私钥存储在安全硬件中,攻击者拿到密钥的难度很大。
再想想实际应用场景
场景1:
设想一个应用有记住用户名、密码的功能。简单的实现是,将用户名密码加密后存储于文件中。加密的密钥是明文写在java代码中。
我想了想,觉得这样其实很不安全。攻击者很容易将文件取到,而安卓客户端apk是公开的。拿到以后反编译,将文件输入,很容易拿到存储于文件的用户名和密码。
对于攻击者,java代码几乎就是公开的。混淆只是增加了阅读理解的难度。
场景2:
如果不是java加密,而是c层加密,会怎么样?
这样也不安全。c层的确无法反编译。但是用户只要拿到解密的方法,通过java层获知调用方式,将文件输入,即可拿到明文。他不需要知道c层的实现。
场景3:
Java后台Spring等一些其他后台系统,实现了remember me机制。客户端需要保存remember me的加密字符串,你可以可以叫它token。这个还相对高级,token不是用户名密码。但是如果token泄漏,也有相对短时间的危险。
如果token的机制比较弱,那么泄漏token就跟泄漏用户名、密码一样危险了。
那么我们可以使用安卓密钥系统了。
2. 加密过程中的主要类
这里把加密的功能分为两块:密钥管理和加密算法
2.1 密钥存储
通常加密算法都有密钥,这个玩意很重要。一般算法都是公开的,密钥是私有的。通常密文是公开的,或者近似公开的,或者保护的不好。
那么保密工作的重点,就是保护密钥。一旦密钥泄漏,那么很多本来加密的信息,都变成公开的了。
安卓中,Key类表示各种密钥,SecretKey类表示对称加密密钥。
使用AES对称加密算法,必须有个SecretKey。
密钥一般是特定长度的字节数组。人们通常用一个字符串来作为key,实际上使用时,要将这个字符串转化为字节数组。
比较简单的做法是把key字符串明文写在java代码中。如果是在服务器代码中,问题不大。如果是在安卓代码中,这就是非常垃圾的做法了。客户端java代码近乎于公开,非常容易反编译。这样做等于把密钥送给了攻击者。整个加密系统,一点用处没有。不如不做,浪费劳动力。
有一种做法是,客户端不保存密钥,需要使用时,从服务器请求,密钥主要存在客户端内存中。这依赖于与服务器通信机制会不会泄漏。
从安卓6.0开始,提供了KeyStore来保存密钥,就是一个密钥仓库。
说一下与兄弟KeyChain的区别。两者都是密钥仓库。KeySore一般是用于应用存储独有密钥,KeyChain用于应用建共享密钥。
2.2 密钥生成
密钥生成,你可以简单的,自己胡乱写一串字符串。按不安全我就不知道了。
安卓提供了KeyGenerator帮助你生成密钥。
KeyGenerator有两个静态方法获取实例:
getInstance(String algorithm)
getInstance(String algorithm, String provider)
这个provider是什么玩意?加密算法有不同的实现,系统支持不同厂商提供不同的类。
安卓官方文档让我们使用的是“AndroidKeyStore”,安卓官方提供的实现。搭配前面的KeyStore使用,非常棒。
KeyGenerator的文档说从安卓诞生(API1)开始,支持AES。
KeyStore的文档说从安卓4.3(API18)开始,支持AndroidKeyStore。
但是,实际上,如果使用单参数getInstance("AES"),的确可以生成Key。
但是,如果调用getInstance("AES", "AndroidKeyStore")的话,在安卓6.0以下,会报错。
而且,如果调用getInstance("AES", "AndroidKeyStore"),生成的密钥会自动放入KeyStore,整个过程你不需要知道密钥是什么。
上面说了一些注意事项,具体使用看第3部分。
2.3 加密工作
具体的加密工作由Cipher类实现。这玩意配置好以后,可以使用各种算法、各种模式。
然而,小心了,使用前,你最好查下文档,看看支不支持。
3. 安卓密钥库系统
这一段讲安卓密钥库的特殊功能,和加密类的使用方法。
3.1 安卓密钥库可以防止泄漏密钥
从安卓4.3(API18)以后,可以使用密钥库。并后续的版本不断完善。貌似安卓6.0以前的密钥库并不完善,会遇到各种问题。建议从安卓6.0使用下面的描述,之前的版本,就走老路子吧。
利用 Android 密钥库系统,您可以在容器中存储加密密钥,提高从设备中提取密钥的难度。在密钥进入密钥库后,可以不导出密钥,就进行加密操作。
A. 密钥库防止从应用进程和安卓设备中提取密钥。避免在当前设备外使用密钥。
B. 密钥库方式指定密钥使用方式。避免在当前设备上,非法使用密钥。
如何防止整体提取?
A、密钥永不进入应用进程。用户将密文送入密钥库,密钥库返回明文。如果应用进程被攻破,攻击者可以使用密钥,但是无法将密钥提取出来。
B、可以将密钥绑定在安全硬件中。就算操作系统被攻破,用户可以读取内存,攻击者也无法提取密钥。注意B需要硬件支持,低版本安卓可能不支持该功能。
如何防止本机非法使用?
使用密钥要先授权。授权条件有3类:加密方面要求、时间间隔、用户身份验证。
授权有点抽象。举个例子,比如,你必须验证了指纹才能使用密钥,或者验证了锁屏密码才能使用。
你验证了指纹后,只有30秒内才能使用密钥。
在密钥初始化的时候,还可以指定,这个密钥只能用于解密。(可能加密在服务器做的吧。一般对称加密,需要制定密钥既用于加密,又用于解密。)
这些限制条件都是KeyStore来实现的。你需要调用接口配置下。
3.2. 使用密钥库
讲一讲范例代码。可以下载运行看看。
https://github.com/googlesamples/android-ConfirmCredential
第一步是如何保存加密的密钥。
正确的方式是用KeyGenerator 来生成密钥,他生成的密钥是保存在密钥库中,你根本不知道密钥是什么。攻击者也不知道。
下面的方法实现了:
(1)初始化KeyStore,用于存储密钥。KeyStore底层实现应该就是上面说的安全硬件了。
(2)获取KeyGenerator ,并设置此次生成密钥的参数。这里会设置一个KEY_NAME,以后可以拿这个去使用密钥。
(3)keyGenerator.generateKey()生成了密钥。
/** * Creates a symmetric key in the Android Key Store which can only be used after the user has * authenticated with device credentials within the last X seconds. */ private void createKey() { // Generate a key to decrypt payment credentials, tokens, etc. // This will most likely be a registration step for the user when they are setting up your app. try { KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); KeyGenerator keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); // Set the alias of the entry in Android KeyStore where the key will appear // and the constrains (purposes) in the constructor of the Builder keyGenerator.init(new KeyGenParameterSpec.Builder(KEY_NAME, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setUserAuthenticationRequired(true) // Require that the user has unlocked in the last 30 seconds .setUserAuthenticationValidityDurationSeconds(AUTHENTICATION_DURATION_SECONDS) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .build()); keyGenerator.generateKey(); } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException | KeyStoreException | CertificateException | IOException e) { throw new RuntimeException("Failed to create a symmetric key", e); } }
第二步是加密。
(1)用KeyStore和刚才的KEY_NAME就可以获取到密钥。
(2)使用这个密钥就可以加密解密。
/** * Tries to encrypt some data with the generated key in {@link #createKey} which * only works if the user has just authenticated via device credentials. */ private boolean tryEncrypt() { try { KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); SecretKey secretKey = (SecretKey) keyStore.getKey(KEY_NAME, null); Cipher cipher = Cipher.getInstance( KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7); // Try encrypting something, it will only work if the user authenticated within // the last AUTHENTICATION_DURATION_SECONDS seconds. cipher.init(Cipher.ENCRYPT_MODE, secretKey); cipher.doFinal(SECRET_BYTE_ARRAY); // If the user has recently authenticated, you will reach here. showAlreadyAuthenticated(); return true; } catch (UserNotAuthenticatedException e) { // User is not authenticated, let's authenticate with device credentials. showAuthenticationScreen(); return false; } catch (KeyPermanentlyInvalidatedException e) { // This happens if the lock screen has been disabled or reset after the key was // generated after the key was generated. Toast.makeText(this, "Keys are invalidated after created. Retry the purchase\n" + e.getMessage(), Toast.LENGTH_LONG).show(); return false; } catch (BadPaddingException | IllegalBlockSizeException | KeyStoreException | CertificateException | UnrecoverableKeyException | IOException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) { throw new RuntimeException(e); } }