Android外部文件加解密及应用实践
有这样的应用场景,当我们把一些重要文件放到asset文件夹中时,把.apk解压是可以直接拿到这个文件的,一些涉及到重要信息的文件我们并不想被反编译拿去,这个时候需要先对文件进行加密,然后放到Android中的资源目录下,用的时候再解密出来。
现代密码学中,加密系统的安全性是基于密钥的,而不是基于算法,现在介绍一整套加解密及应用流程,这套加密流程从实用性和安全性上来讲,我觉得还是很靠谱的,也是市面上比较常用的做法,核心逻辑其实比较简单,毕竟最难的加解密算法实现部分是现成的了,我司部分也用了这套流程,当然会比我讲的这个要复杂一些。
1、简介
主要涉及到一下几个算法的应用,RSA、AES,以及Base64编码,基本思想是用[AES算法+AES密钥]来加密文件,为了保证密钥的安全性,会通过[RSA算法+RSA私钥]对AES密钥进行加密。
对这几种算法不熟悉可以看看我司大佬的‘常用的加密方式和应用场景’这篇文章,知道大概的原理和使用方法就行,因为算法在java中都是现成的,直接拿来用就是了。
把流程整理了一下,就是以上的流程图,分成三块:
- 第1块是把加密过程给封装成一个小工具,用加密工具来对文件进行加密;
- 第2块是把解密过程封装成解密的小工具,用解密工具来解密我们的文件好进行相关修改;
- 第3块使我们的目的,就是把加密文件和加解密的AES算法密钥放到Android资源文件中进行具体的使用。
有一点需要补充的,就是RSA算法的公私钥,从第3块中可以发现,并没有把RSA的公钥和私钥放到资源文件中,其实大家想想就知道了,如果被加密文件、加解密的AES密钥、用于对AES密钥进行加密的RSA密钥三者都放入文件夹中,那就没有啥安全性可言了(注:加解密的算法可以改造成自己公司独有的,我司就是这么做的),所以为了保证安全性,我们的RSA公私钥是通过应用的签名(.keystore签名文件)中代码动态获取。感兴趣的可以看这篇文章:[从Java Keystore文件中提取私钥、证书]。
2、第1块:加密工具进行加密
工具的java界面开发是通过java的swing包来实现的,对swing感兴趣的可以参考这篇Java Swing 图形界面开发简介,讲得非常详细。
一开始的时候是没有AES秘钥的,需要我们生成一个安全的秘钥,所以生成一个随机AES秘钥,然后保存,加密工具的操作页界面:
2.1、生成随机秘钥
生成随机秘钥主要分为几步:
- 通过UUID.randomUUID()生成随机数作为seed种子;
- seed种子提供给KeyGenerator生成AES秘钥,只要seed种子生成的AES秘钥就是一致的;
- 通过应用签名获取RSA算法需要的公钥私钥;
- RSA通过私钥来加密AES秘钥;
因为生成的秘钥是byte[],所以通过Base64编码展示出来给到界面上。
/** * 生成随机密钥 */ private void randomKey() { try { //生成随机数作为seed种子 String uuid = UUID.randomUUID().toString(); byte[] seed = uuid.getBytes("UTF-8"); //生成AES秘钥 byte[] rawkey = AES.getRawKey(seed); //获取应用签名的密钥对 KeyPair pair = SignKey.getSignKeyPair(); //通过RSA私钥来加密AES秘钥 byte[] key = RSA.encrypt(rawkey, pair.getPrivate()); //Base64编码成字符串展示 String base64Key = Base64.encode(key); mKeyText.setText(base64Key); } catch (Exception e) { e.printStackTrace(); } } 其中AES.getRawKey(seed)中主要是通过AES密钥生成器来生成128位的密钥,具体实现: /** * 生成用AES算法来加密的密钥流,这个密钥会被应用签名{@link SignKey}的密钥进行二次加密 */ public static byte[] getRawKey(byte[] seed) throws Exception { KeyGenerator kgen = KeyGenerator.getInstance("AES"); SecureRandom sr = SecureRandom.getInstance("SHA1PRNG"); sr.setSeed(seed); //192 and 256 bits may not be available kgen.init(128, sr); SecretKey skey = kgen.generateKey(); return skey.getEncoded(); }
SignKey.getSignKeyPair()是获得RSA算法所需的公私钥,是从我们的应用签名来的,大家应该都很熟悉了,应用打包上传是需要签名打包的。
java提供了api获取testkey.keystore文件(自己用studio生成一个)的私钥和证书,把testkey.keystore文件放到目录中:
/** * Author:xishuang * Date:2018.05.06 * Des:根据导入的应用签名,读取其中的密钥对和证书 */ public class SignKey { //应用签名 private static final String keystoreName = "testkey.keystore"; private static final String keystorePassword = "123456"; //应用签名的别名 private static final String alias = "key0"; private static final String aliasPassword = "123456"; /** * 获取签名的密钥对,用来给密钥加密 */ public static KeyPair getSignKeyPair() { try { File storeFile = new File(keystoreName); if (!storeFile.exists()) { throw new IllegalArgumentException("还没设置签名文件!"); } String keyStoreType = "JKS"; char[] keystorepasswd = keystorePassword.toCharArray(); char[] keyaliaspasswd = aliasPassword.toCharArray(); KeyStore keystore = KeyStore.getInstance(keyStoreType); keystore.load(new FileInputStream(storeFile), keystorepasswd); //拿私钥 Key key = keystore.getKey(alias, keyaliaspasswd); if (key instanceof PrivateKey) { //拿公钥 Certificate cert = keystore.getCertificate(alias); PublicKey publicKey = cert.getPublicKey(); ///公私钥存到KeyPair return new KeyPair(publicKey, (PrivateKey) key); } } catch (Exception e) { e.printStackTrace(); } return null; } }
拿testkey.keystore所需的参数都在跟我们打包应用签名所需一样,通过java提供的keystore类获取。然后就是用刚拿到的testkey.keystore私钥来加密AES密钥,再通过Base64转换一下编码成字符串展示出来,只是为了把密钥展示出来才转换编码的。
2.2、导出密钥
把密钥导出成文件,下次直接导入密钥用来解密文件,导出密钥需要先用Base64把文本框里的Base64密钥字符串转换为Byte[]再存。
byte[] key = Base64.decode(base64Key); //将raw key输出 File keyFile = new File(dir, "testkey.dat"); FileOutputStream fos = new FileOutputStream(keyFile);
2.3、加密文件
密钥已有,AES算法又是现成的,直接调用api加密就行了:
private static final String AES = "AES"; /** * AES算法加密文件 * * @param rawKey AES密钥 * @param fromFile 要加密的文件 * @param toFile 加密后文件 */ public static void encryptFile(byte[] rawKey, File fromFile, File toFile) throws Exception { if (!fromFile.exists()) { throw new NullPointerException("文件不存在"); } if (toFile.exists()) { toFile.delete(); } SecretKeySpec skeySpec = new SecretKeySpec(rawKey, AES); Cipher cipher = Cipher.getInstance(AES); //加密模式 cipher.init(Cipher.ENCRYPT_MODE, skeySpec); FileInputStream fis = new FileInputStream(fromFile); FileOutputStream fos = new FileOutputStream(toFile, true); byte[] buffer = new byte[512 * 1024 - 16]; int offset; //使用加密流来加密 CipherInputStream bis = new CipherInputStream(fis, cipher); while ((offset = bis.read(buffer)) != -1) { fos.write(buffer, 0, offset); fos.flush(); } fos.close(); fis.close(); }
选择文件,通过AES算法和AES密钥加密,最后效果如下,没有密钥能解密出来算我输。
3、第2块:解密工具进行解密
解密过程其实没啥必要讲了,因为解密过程是加密过程的逆过程。
这个解密不是在应用中用的,是为了便于我们更新加密文件,修改文件之前必须要先把文件先解密。
3.1、导入AES密钥
这个密钥就是我们前面生成的密钥,导进来后用应用签名的RSA公钥解密AES密钥即可:
//获取被加密的密钥raw key String keyStr = mKeyText.getText(); byte[] key = Base64.decode(keyStr); //获取应用签名密钥对,公钥解密raw key KeyPair keypair = SignKey.getSignKeyPair(); byte[] rawkey = RSA.decrypt(key, keypair.getPublic()); //用raw key去解密文件 AES.decryptFile(rawkey, fromFile, toFile);
3.2、解密文件
拿到纯洁版AES密钥之后就可以直接调用AES算法解密文件了:
/** * AES算法解密文件 * * @param rawKey AES密钥 * @param fromFile 被加密的文件 * @param toFile 解密后文件 */ public static void decryptFile(byte[] rawKey, File fromFile, File toFile) throws Exception { if (!fromFile.exists()) { throw new NullPointerException("文件不存在"); } if (toFile.exists()) { toFile.delete(); } SecretKeySpec skeySpec = new SecretKeySpec(rawKey, AES); Cipher cipher = Cipher.getInstance(AES); //解密模式 cipher.init(Cipher.DECRYPT_MODE, skeySpec); FileInputStream fis = new FileInputStream(fromFile); FileOutputStream fos = new FileOutputStream(toFile, true); byte[] buffer = new byte[512 * 1024 + 16]; int offset; //使用解密流来解密 CipherInputStream cipherInputStream = new CipherInputStream(fis, cipher); while ((offset = cipherInputStream.read(buffer)) != -1) { fos.write(buffer, 0, offset); fos.flush(); } fos.close(); fis.close(); }
和AES加密过程一对比,会发现只是切换一下AES算法模式。
3、第3块:Android应用中解密文件
要解密文件,需要在资源文件夹中加入被加密的AES密钥,这个密钥就是上面导出来的,还有就是被加密后的文件。能正确解密的前提是你应用签名和用来给文件加密过程中用到的签名是同一个。
3.1、解密AES密钥
在Android应用中解密文件与在java工具中解密文件,区别主要在于RSA密钥的获取,在java工具中应用签名testkey.keystore是开发者拥有的,可以拿到其中的全部信息,而在Android中应用是要发布到应用市场的,任何人都可以下载我们的包,应用签名只能通过Android提供的api拿到其公钥。
/** * Author:xishuang * Date:2018.05.06 * Des:应用签名读取工具类 */ public class SignKey { /** * 获取当前应用的签名 * * @param context 上下文 */ public static byte[] getSign(Context context) { PackageManager pm = context.getPackageManager(); try { PackageInfo info = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES); Signature[] signatures = info.signatures; if (signatures != null) { return signatures[0].toByteArray(); } } catch (NameNotFoundException e) { e.printStackTrace(); } return null; } /** * 根据签名去获取公钥 */ public static PublicKey getPublicKey(byte[] signature) { try { CertificateFactory certFactory = CertificateFactory .getInstance("X.509"); X509Certificate cert = (X509Certificate) certFactory .generateCertificate(new ByteArrayInputStream(signature)); return cert.getPublicKey(); } catch (CertificateException e) { e.printStackTrace(); } return null; } }
拿到应用签名testkey.keystore的公钥之后的流程就和在java工具中的操作基本一致了,用RSA公钥来解密AES密钥。
private static final String SIMPLE_KEY_DATA = "testkey.dat"; /** * 获取解密之后的文件加密密钥 */ private static byte[] getRawKey(Context context) throws Exception { //获取应用的签名密钥 byte[] sign = SignKey.getSign(context); PublicKey pubKey = SignKey.getPublicKey(sign); //获取加密文件的密钥 InputStream keyis = context.getAssets().open(SIMPLE_KEY_DATA); byte[] key = getData(keyis); //解密密钥 return RSA.decrypt(key, pubKey); }
最后再用解密之后的AES密钥来解密文件。
3.2、AES密钥解密文件
通过资源管理器拿到加密文件的文件流,通过AES密钥来用AES算法来解密文件流。
/** * 获取解密之后的文件流 */ public static InputStream onObtainInputStream(Context context) { try { AssetManager assetmanager = context.getAssets(); InputStream is = assetmanager.open("encrypt_测试.txt"); byte[] rawkey = getRawKey(context); //使用解密流,数据写出到基础OutputStream之前先对该会先对数据进行解密 SecretKeySpec skeySpec = new SecretKeySpec(rawkey, "AES"); Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.DECRYPT_MODE, skeySpec); return new CipherInputStream(is, cipher); } catch (Exception e) { e.printStackTrace(); } return null; }
拿到加密后文件流之后就达成目的了,可以解析成字符串展示出来:
private void inputData() { InputStream in = DecryptUtil.onObtainInputStream(this); try { BufferedReader reader = new BufferedReader(new InputStreamReader(in, "GBK")); StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line + "\n"); } contentTv.setText(sb.toString()); } catch (IOException e) { e.printStackTrace(); } finally { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } }
实例效果图如下,请关注红框里面内容,因为懒得新建项目,用原有项目测试了一下:
目前工具使用的是市面上比较常见的加解密算法,可以换一下算法,比如DES或者其它的对称和非对称算法,甚至是自己改动的算法,想运行示例演示的话: