Android外部文件加解密及應(yīng)用實(shí)踐
有這樣的應(yīng)用場景,當(dāng)我們把一些重要文件放到asset文件夾中時(shí),把.apk解壓是可以直接拿到這個(gè)文件的,一些涉及到重要信息的文件我們并不想被反編譯拿去,這個(gè)時(shí)候需要先對文件進(jìn)行加密,然后放到Android中的資源目錄下,用的時(shí)候再解密出來。
現(xiàn)代密碼學(xué)中,加密系統(tǒng)的安全性是基于密鑰的,而不是基于算法,現(xiàn)在介紹一整套加解密及應(yīng)用流程,這套加密流程從實(shí)用性和安全性上來講,我覺得還是很靠譜的,也是市面上比較常用的做法,核心邏輯其實(shí)比較簡單,畢竟最難的加解密算法實(shí)現(xiàn)部分是現(xiàn)成的了,我司部分也用了這套流程,當(dāng)然會比我講的這個(gè)要復(fù)雜一些。
1、簡介
主要涉及到一下幾個(gè)算法的應(yīng)用,RSA、AES,以及Base64編碼,基本思想是用[AES算法+AES密鑰]來加密文件,為了保證密鑰的安全性,會通過[RSA算法+RSA私鑰]對AES密鑰進(jìn)行加密。
對這幾種算法不熟悉可以看看我司大佬的‘常用的加密方式和應(yīng)用場景’這篇文章,知道大概的原理和使用方法就行,因?yàn)樗惴ㄔ趈ava中都是現(xiàn)成的,直接拿來用就是了。
把流程整理了一下,就是以上的流程圖,分成三塊:
- 第1塊是把加密過程給封裝成一個(gè)小工具,用加密工具來對文件進(jìn)行加密;
- 第2塊是把解密過程封裝成解密的小工具,用解密工具來解密我們的文件好進(jìn)行相關(guān)修改;
- 第3塊使我們的目的,就是把加密文件和加解密的AES算法密鑰放到Android資源文件中進(jìn)行具體的使用。
有一點(diǎn)需要補(bǔ)充的,就是RSA算法的公私鑰,從第3塊中可以發(fā)現(xiàn),并沒有把RSA的公鑰和私鑰放到資源文件中,其實(shí)大家想想就知道了,如果被加密文件、加解密的AES密鑰、用于對AES密鑰進(jìn)行加密的RSA密鑰三者都放入文件夾中,那就沒有啥安全性可言了(注:加解密的算法可以改造成自己公司獨(dú)有的,我司就是這么做的),所以為了保證安全性,我們的RSA公私鑰是通過應(yīng)用的簽名(.keystore簽名文件)中代碼動態(tài)獲取。感興趣的可以看這篇文章:[從Java Keystore文件中提取私鑰、證書]。
2、第1塊:加密工具進(jìn)行加密
工具的java界面開發(fā)是通過java的swing包來實(shí)現(xiàn)的,對swing感興趣的可以參考這篇Java Swing 圖形界面開發(fā)簡介,講得非常詳細(xì)。
一開始的時(shí)候是沒有AES秘鑰的,需要我們生成一個(gè)安全的秘鑰,所以生成一個(gè)隨機(jī)AES秘鑰,然后保存,加密工具的操作頁界面:
2.1、生成隨機(jī)秘鑰
生成隨機(jī)秘鑰主要分為幾步:
- 通過UUID.randomUUID()生成隨機(jī)數(shù)作為seed種子;
- seed種子提供給KeyGenerator生成AES秘鑰,只要seed種子生成的AES秘鑰就是一致的;
- 通過應(yīng)用簽名獲取RSA算法需要的公鑰私鑰;
- RSA通過私鑰來加密AES秘鑰;
因?yàn)樯傻拿罔€是byte[],所以通過Base64編碼展示出來給到界面上。
- /**
- * 生成隨機(jī)密鑰
- */
- private void randomKey() {
- try {
- //生成隨機(jī)數(shù)作為seed種子
- String uuid = UUID.randomUUID().toString();
- byte[] seed = uuid.getBytes("UTF-8");
- //生成AES秘鑰
- byte[] rawkey = AES.getRawKey(seed);
- //獲取應(yīng)用簽名的密鑰對
- 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位的密鑰,具體實(shí)現(xiàn):
- /**
- * 生成用AES算法來加密的密鑰流,這個(gè)密鑰會被應(yīng)用簽名{@link SignKey}的密鑰進(jìn)行二次加密
- */
- 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算法所需的公私鑰,是從我們的應(yīng)用簽名來的,大家應(yīng)該都很熟悉了,應(yīng)用打包上傳是需要簽名打包的。
java提供了api獲取testkey.keystore文件(自己用studio生成一個(gè))的私鑰和證書,把testkey.keystore文件放到目錄中:
- /**
- * Author:xishuang
- * Date:2018.05.06
- * Des:根據(jù)導(dǎo)入的應(yīng)用簽名,讀取其中的密鑰對和證書
- */
- public class SignKey {
- //應(yīng)用簽名
- private static final String keystoreName = "testkey.keystore";
- private static final String keystorePassword = "123456";
- //應(yīng)用簽名的別名
- 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("還沒設(shè)置簽名文件!");
- }
- 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ù)都在跟我們打包應(yīng)用簽名所需一樣,通過java提供的keystore類獲取。然后就是用剛拿到的testkey.keystore私鑰來加密AES密鑰,再通過Base64轉(zhuǎn)換一下編碼成字符串展示出來,只是為了把密鑰展示出來才轉(zhuǎn)換編碼的。
2.2、導(dǎo)出密鑰
把密鑰導(dǎo)出成文件,下次直接導(dǎo)入密鑰用來解密文件,導(dǎo)出密鑰需要先用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塊:解密工具進(jìn)行解密
解密過程其實(shí)沒啥必要講了,因?yàn)榻饷苓^程是加密過程的逆過程。
這個(gè)解密不是在應(yīng)用中用的,是為了便于我們更新加密文件,修改文件之前必須要先把文件先解密。
3.1、導(dǎo)入AES密鑰
這個(gè)密鑰就是我們前面生成的密鑰,導(dǎo)進(jìn)來后用應(yīng)用簽名的RSA公鑰解密AES密鑰即可:
- //獲取被加密的密鑰raw key
- String keyStr = mKeyText.getText();
- byte[] key = Base64.decode(keyStr);
- //獲取應(yīng)用簽名密鑰對,公鑰解密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應(yīng)用中解密文件
要解密文件,需要在資源文件夾中加入被加密的AES密鑰,這個(gè)密鑰就是上面導(dǎo)出來的,還有就是被加密后的文件。能正確解密的前提是你應(yīng)用簽名和用來給文件加密過程中用到的簽名是同一個(gè)。
3.1、解密AES密鑰
在Android應(yīng)用中解密文件與在java工具中解密文件,區(qū)別主要在于RSA密鑰的獲取,在java工具中應(yīng)用簽名testkey.keystore是開發(fā)者擁有的,可以拿到其中的全部信息,而在Android中應(yīng)用是要發(fā)布到應(yīng)用市場的,任何人都可以下載我們的包,應(yīng)用簽名只能通過Android提供的api拿到其公鑰。
- /**
- * Author:xishuang
- * Date:2018.05.06
- * Des:應(yīng)用簽名讀取工具類
- */
- public class SignKey {
- /**
- * 獲取當(dāng)前應(yīng)用的簽名
- *
- * @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;
- }
- }
拿到應(yīng)用簽名testkey.keystore的公鑰之后的流程就和在java工具中的操作基本一致了,用RSA公鑰來解密AES密鑰。
- private static final String SIMPLE_KEY_DATA = "testkey.dat";
- /**
- * 獲取解密之后的文件加密密鑰
- */
- private static byte[] getRawKey(Context context) throws Exception {
- //獲取應(yīng)用的簽名密鑰
- 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ù)寫出到基礎(chǔ)OutputStream之前先對該會先對數(shù)據(jù)進(jìn)行解密
- 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;
- }
拿到加密后文件流之后就達(dá)成目的了,可以解析成字符串展示出來:
- 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();
- }
- }
- }
實(shí)例效果圖如下,請關(guān)注紅框里面內(nèi)容,因?yàn)閼械眯陆?xiàng)目,用原有項(xiàng)目測試了一下:
目前工具使用的是市面上比較常見的加解密算法,可以換一下算法,比如DES或者其它的對稱和非對稱算法,甚至是自己改動的算法,想運(yùn)行示例演示的話:
就是運(yùn)行一下java文件,就可以打開加解密小工具了,加解密工具界面是仿我司工具包中抽出來的小部分的,畢竟寫界面好煩,感謝我司大神多年前就寫出了如此工具。