Android外部文件加解密及應用實踐
有這樣的應用場景,當我們把一些重要文件放到asset文件夾中時,把.apk解壓是可以直接拿到這個文件的,一些涉及到重要信息的文件我們并不想被反編譯拿去,這個時候需要先對文件進行加密,然后放到Android中的資源目錄下,用的時候再解密出來。
現(xiàn)代密碼學中,加密系統(tǒng)的安全性是基于密鑰的,而不是基于算法,現(xiàn)在介紹一整套加解密及應用流程,這套加密流程從實用性和安全性上來講,我覺得還是很靠譜的,也是市面上比較常用的做法,核心邏輯其實比較簡單,畢竟最難的加解密算法實現(xiàn)部分是現(xiàn)成的了,我司部分也用了這套流程,當然會比我講的這個要復雜一些。
1、簡介
主要涉及到一下幾個算法的應用,RSA、AES,以及Base64編碼,基本思想是用[AES算法+AES密鑰]來加密文件,為了保證密鑰的安全性,會通過[RSA算法+RSA私鑰]對AES密鑰進行加密。
對這幾種算法不熟悉可以看看我司大佬的‘常用的加密方式和應用場景’這篇文章,知道大概的原理和使用方法就行,因為算法在java中都是現(xiàn)成的,直接拿來用就是了。
把流程整理了一下,就是以上的流程圖,分成三塊:
- 第1塊是把加密過程給封裝成一個小工具,用加密工具來對文件進行加密;
- 第2塊是把解密過程封裝成解密的小工具,用解密工具來解密我們的文件好進行相關修改;
- 第3塊使我們的目的,就是把加密文件和加解密的AES算法密鑰放到Android資源文件中進行具體的使用。
有一點需要補充的,就是RSA算法的公私鑰,從第3塊中可以發(fā)現(xiàn),并沒有把RSA的公鑰和私鑰放到資源文件中,其實大家想想就知道了,如果被加密文件、加解密的AES密鑰、用于對AES密鑰進行加密的RSA密鑰三者都放入文件夾中,那就沒有啥安全性可言了(注:加解密的算法可以改造成自己公司獨有的,我司就是這么做的),所以為了保證安全性,我們的RSA公私鑰是通過應用的簽名(.keystore簽名文件)中代碼動態(tài)獲取。感興趣的可以看這篇文章:[從Java Keystore文件中提取私鑰、證書]。
2、第1塊:加密工具進行加密
工具的java界面開發(fā)是通過java的swing包來實現(xiàn)的,對swing感興趣的可以參考這篇Java Swing 圖形界面開發(fā)簡介,講得非常詳細。
一開始的時候是沒有AES秘鑰的,需要我們生成一個安全的秘鑰,所以生成一個隨機AES秘鑰,然后保存,加密工具的操作頁界面:
2.1、生成隨機秘鑰
生成隨機秘鑰主要分為幾步:
- 通過UUID.randomUUID()生成隨機數(shù)作為seed種子;
- seed種子提供給KeyGenerator生成AES秘鑰,只要seed種子生成的AES秘鑰就是一致的;
- 通過應用簽名獲取RSA算法需要的公鑰私鑰;
- RSA通過私鑰來加密AES秘鑰;
因為生成的秘鑰是byte[],所以通過Base64編碼展示出來給到界面上。
- /**
- * 生成隨機密鑰
- */
- private void randomKey() {
- try {
- //生成隨機數(shù)作為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位的密鑰,具體實現(xiàn):
- /**
- * 生成用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:根據(jù)導入的應用簽名,讀取其中的密鑰對和證書
- */
- 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所需的參數(shù)都在跟我們打包應用簽名所需一樣,通過java提供的keystore類獲取。然后就是用剛拿到的testkey.keystore私鑰來加密AES密鑰,再通過Base64轉(zhuǎn)換一下編碼成字符串展示出來,只是為了把密鑰展示出來才轉(zhuǎn)換編碼的。
2.2、導出密鑰
把密鑰導出成文件,下次直接導入密鑰用來解密文件,導出密鑰需要先用Base64把文本框里的Base64密鑰字符串轉(zhuǎn)換為Byte[]再存。
- byte[] key = Base64.decode(base64Key);
- //將raw key輸出
- File keyFile = new File(dir, "testkey.dat");
- FileOutputStream fos = new FileOutputStream(keyFile);
2.3、加密文件
密鑰已有,AES算法又是現(xiàn)成的,直接調(diào)用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密鑰之后就可以直接調(diào)用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加密過程一對比,會發(fā)現(xiàn)只是切換一下AES算法模式。
3、第3塊:Android應用中解密文件
要解密文件,需要在資源文件夾中加入被加密的AES密鑰,這個密鑰就是上面導出來的,還有就是被加密后的文件。能正確解密的前提是你應用簽名和用來給文件加密過程中用到的簽名是同一個。
3.1、解密AES密鑰
在Android應用中解密文件與在java工具中解密文件,區(qū)別主要在于RSA密鑰的獲取,在java工具中應用簽名testkey.keystore是開發(fā)者擁有的,可以拿到其中的全部信息,而在Android中應用是要發(fā)布到應用市場的,任何人都可以下載我們的包,應用簽名只能通過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;
- }
- /**
- * 根據(jù)簽名去獲取公鑰
- */
- 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);
- //使用解密流,數(shù)據(jù)寫出到基礎OutputStream之前先對該會先對數(shù)據(jù)進行解密
- 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();
- }
- }
- }
實例效果圖如下,請關注紅框里面內(nèi)容,因為懶得新建項目,用原有項目測試了一下:
目前工具使用的是市面上比較常見的加解密算法,可以換一下算法,比如DES或者其它的對稱和非對稱算法,甚至是自己改動的算法,想運行示例演示的話:
就是運行一下java文件,就可以打開加解密小工具了,加解密工具界面是仿我司工具包中抽出來的小部分的,畢竟寫界面好煩,感謝我司大神多年前就寫出了如此工具。