Android 關於資料加密和編碼的總結
最近正好在專案中用到資料加密,於是從網上查閱一些資料,瞭解各種加密方式並寫程式碼驗證,就在本篇文章中做個總結吧。
我將從這幾個方面介紹 Android 中的加密方式以及相關的概念:
1. 異或加密
2. MD5 演算法
3. Base64 編碼
4. DES 加密
5. AES 加密
6. RSA 加密
從嚴格意義上來說,MD5 和 Base64 不屬於加密,它們分別是資訊摘要演算法和編碼方式,但是網上好多人都說 MD5 加密、Base64 加密,我覺得有必要糾正一下。對於其他的幾種加密方式,下面我會一一進行舉例說明。
1. 異或加密
異或運算(xor)有個特點:數 a 兩次異或同一個數 b 仍然為 a,即 (a^b)^b=a
。利用這個原理可以實現資料的加密和解密功能。
舉個栗子:a=10100001,b=00000110
a=a^b; // a=10100111 b=b^a; // b=10100001,此時 b 等於 a
異或運算直接對二進位制資料進行操作,對每一位(bit)上的資料進行變換。所以輸入和輸出的資料長度相同,不佔用額外的空間,可以用於字元和檔案的加密,效率比較高。下面我們用程式碼實踐一下:
// 加密的金鑰,構造一定長度的位元組陣列 private final static byte[] KEY_BYTES = "Vp6flFpGW86g7hi6MhD3Zl2eThJTjPnIjXE4".getBytes(); private final static int KEY_LENGTH = KEY_BYTES.length; /** * 異或運算加密 * * @param input 要加密的內容 * @return 加密後的資料 */ public static byte[] xorEncode(byte[] input) { int keyIndex = 0; int length = input.length; for (int i = 0; i < length; i++) { input[i] = (byte) (input[i] ^ KEY_BYTES[(keyIndex++ % KEY_LENGTH)]); } return input; } /** * 異或運算解密 * * @param input 要解密的內容 * @return 解密後的資料 */ public static byte[] xorDecode(byte[] input) { int keyIndex = 0; int length = input.length; for (int i = 0; i < length; i++) { input[i] = (byte) (input[i] ^ KEY_BYTES[(keyIndex++ % KEY_LENGTH)]); } return input; }
為了方便檢視加密後的內容,這裡對輸出結果做了一下 Base64 編碼。
輸入:123456abcdef,輸出:Z0IFUl1aJzooFCIx
2. MD5 編碼
MD5 是將任意長度的資料字串轉化成短小的固定長度的值的單向操作,任意兩個字串不應有相同的雜湊值。因此 MD5 經常用於校驗字串或者檔案,因為如果檔案的 MD5 不一樣,說明檔案內容也是不一樣的,如果發現下載的檔案和給定的 MD5 值不一樣,就要慎重使用。
MD5 主要用做資料一致性驗證、數字簽名和安全訪問認證,而不是用作加密。比如說使用者在某個網站註冊賬戶時,輸入的密碼一般經過 MD5 編碼,更安全的做法還會加一層鹽(salt),這樣密碼就具有不可逆性。然後把編碼後的密碼存入資料庫,下次登入的時候把密碼 MD5 編碼,然後和資料庫中的作對比,這樣就提升了使用者賬戶的安全性。
使用 Java 實現簡單的 MD5 編碼:
/** * 計算字串的 MD5 * * @param text 原文 * @return 密文 */ public static String md5Encode(String text) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(text.getBytes()); StringBuilder sb = new StringBuilder(); for (byte b : digest) { String hexString = Integer.toHexString(b & 0xFF); if (hexString.length() == 1) { hexString = "0" + hexString; } sb.append(hexString); } return sb.toString(); } catch (NoSuchAlgorithmException e) { logger.error(e); } return null; }
輸入:123456abcdef,輸出:6f3b8ded65bd7a4db11625ac84e579bb
3. Base64 編碼
Base64 編碼是我們程式開發中經常使用到的編碼方法,它用 64 個可列印字元來表示二進位制資料。這 64 個字元是:小寫字母 a-z、大寫字母 A-Z、數字 0-9、符號"+"、"/"(再加上作為墊字的"=",實際上是 65 個字元),其他所有符號都轉換成這個字符集中的字元。Base64 編碼通常用作儲存、傳輸一些二進位制資料編碼方法,所以說它本質上是一種將二進位制資料轉成文字資料的方案。
在 Android 中使用 Base64 很簡單,系統的 API 已經封裝好方法,我們直接呼叫即可。
// 編碼 String encode = Base64.encodeToString(" 123456abcdef".getBytes(), Base64.DEFAULT); // 解碼 byte[] decodeByte = Base64.decode(encode .getBytes(), Base64.DEFAULT); String decode = new String(decodeByte);
輸入:123456abcdef,輸出:MTIzNDU2YWJjZGVm
無論是編碼還是解碼都會有一個引數 flags,系統 API 提供了以下幾種:
- DEFAULT:表示使用預設的方法來加密。
- NO_PADDING:表示省略加密字串最後的 "="。
- NO_WRAP:表示省略所有的換行符(設定後 CRLF 就會失去作用)。
- CRLF:表示使用 CR、LF 這一對作為一行的結尾而不是 Unix 風格的 LF。
- URL_SAFE:表示加密時不使用對 URL 和檔名有特殊意義的字元來作為加密字元,就是以 "-" 和 "_" 代替 "+" 和 "/"。
4. DES 加密
DES 是一種對稱加密演算法,所謂對稱加密演算法就是:加密和解密使用相同金鑰的演算法。DES 加密演算法出自 IBM 的研究,後來被美國政府正式採用,之後開始廣泛流傳。但近些年使用越來越少,因為 DES 使用 56 位金鑰,以現代的計算能力,24 小時內即可被破解。
順便說一下 3DES(Triple DES),它是 DES 向 AES 過渡的加密演算法,使用 3 條 56 位的金鑰對資料進行三次加密。是 DES 的一個更安全的變形。它以 DES 為基本模組,通過組合分組方法設計出分組加密演算法。比起最初的 DES,3DES 更為安全。
使用 Java 實現 DES 加密解密,注意密碼長度要是 8 的倍數。加密和解密的 Cipher 構造引數一定要相同,不然會報錯。
/* 加密使用的 key */ private final static byte[] KEY_BYTES = "Vp6fhlFXKpGW8k6QPRg7Q6Jb7HyAhRi6MIhJ2YtGD3Zl26eTthJTj5PnIjXH5EI4".getBytes(); /** * DES 加密 * * @param content 待加密內容 * @param key加密的金鑰 * @return 加密後的位元組陣列 */ public static byte[] encryptDES(byte[] content, byte[] key) { try { SecureRandom random = new SecureRandom(); DESKeySpec desKey = new DESKeySpec(key); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); SecretKey secretKey = keyFactory.generateSecret(desKey); // DES 是加密方式, EBC 是工作模式, PKCS5Padding 是填充模式 Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, secretKey, random); return cipher.doFinal(content); } catch (Exception e) { logger.error(e); } return null; } /** * DES 解密 * * @param content 待解密內容 * @param key解密的金鑰 * @return 解密的資料 */ public static byte[] decryptDES(byte[] content, byte[] key) { try { SecureRandom random = new SecureRandom(); DESKeySpec desKey = new DESKeySpec(key); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); SecretKey secretKey = keyFactory.generateSecret(desKey); Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, secretKey, random); return cipher.doFinal(content); } catch (Exception e) { logger.error(e); } return null; }
輸入:123456abcdef,輸出:j1kR1+ZraO2Tg78dHueoTg==
5. AES 加密
高階加密標準(英語:Advanced Encryption Standard,縮寫:AES),在密碼學中又稱 Rijndael 加密法,是美國聯邦政府採用的一種區塊加密標準。這個標準用來替代原先的 DES,已經被多方分析且廣為全世界所使用。簡單說就是 DES 的增強版,比 DES 的加密強度更高。
AES 與 DES 一樣,一共有四種加密模式:電子密碼本模式(ECB)、加密分組連結模式(CBC)、加密反饋模式(CFB)和輸出反饋模式(OFB)。關於加密模式的介紹,推薦這篇文章: 高階加密標準AES的工作模式(ECB、CBC、CFB、OFB)
/* 加密使用的 key */ private static final String AES_KEY = "KUbHwTqBy6TBQ2gN"; /* 加密使用的 IV */ private static final String AES_IV = "pIbF6GR3XEN1PG05"; /** * AES 加密 * * @param content 待解密內容 * @param key金鑰 * @return 解密的資料 */ public static byte[] encryptAES(byte[] content, byte[] key) { try { SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); // AES 是加密方式, CBC 是工作模式, PKCS5Padding 是填充模式 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); // IV 是初始向量,可以增強密碼的強度 cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, new IvParameterSpec(AES_IV.getBytes())); return cipher.doFinal(content); } catch (Exception e) { logger.error(e); } return null; } /** * AES 解密 * * @param content 待解密內容 * @param key金鑰 * @return 解密的資料 */ public static byte[] decryptAES(byte[] content, byte[] key) { try { SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(AES_IV.getBytes())); return cipher.doFinal(content); } catch (Exception e) { logger.error(e); } return null; }
輸入:123456abcdef,輸出:ho9cn9SvmeisfJy6Pv96oQ==
6. RSA 加密
RSA演算法是一種非對稱加密演算法,所謂非對稱就是該演算法需要一對金鑰,若使用其中一個加密,則需要用另一個才能解密。目前它是最有影響力和最常用的公鑰加密演算法,能夠抵抗已知的絕大多數密碼攻擊。從提出到現今的三十多年裡,經歷了各種攻擊的考驗,逐漸為人們接受,普遍認為是目前最優秀的公鑰方案之一。
該演算法基於一個的數論事實:將兩個大質數相乘十分容易,但是想要對其乘積進行因式分解卻極其困難,因此可以將乘積公開作為加密金鑰。由於進行的都是大數計算,RSA 最快的情況也比 DES 慢上好幾倍,比對應同樣安全級別的對稱密碼演算法要慢 1000 倍左右。所以 RSA 一般只用於少量資料加密,比如說交換對稱加密的金鑰。
使用 RSA 加密主要有這麼幾步:生成金鑰對、公開公鑰、公鑰加密私鑰解密、私鑰加密公鑰解密。
public static final String AES = "AES"; public static final String ECB_PKCS1_PADDING = "RSA/ECB/PKCS1Padding"; /** * 隨機生成 RSA 金鑰對 * * @param keyLength 金鑰長度, 範圍: 512~2048, 一般 1024 * @return 金鑰對 */ public static KeyPair generateRSAKeyPair(int keyLength) { try { KeyPairGenerator kpg = KeyPairGenerator.getInstance(RSA); kpg.initialize(keyLength); return kpg.genKeyPair(); } catch (NoSuchAlgorithmException e) { logger.error(e); } return null; } /** * 公鑰加密 * * @param data原文 * @param publicKey 公鑰 * @return 加密後的資料 */ public static byte[] encryptByPublicKey(byte[] data, byte[] publicKey) { X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey); try { KeyFactory kf = KeyFactory.getInstance(RSA); PublicKey keyPublic = kf.generatePublic(keySpec); Cipher cipher = Cipher.getInstance(ECB_PKCS1_PADDING); cipher.init(Cipher.ENCRYPT_MODE, keyPublic); return cipher.doFinal(data); } catch (Exception e) { logger.error(e); } return null; } /** * 私鑰加密 * * @param data待加密資料 * @param privateKey 金鑰 * @return 加密後的資料 */ public static byte[] encryptByPrivateKey(byte[] data, byte[] privateKey) { PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKey); try { KeyFactory kf = KeyFactory.getInstance(RSA); PrivateKey keyPrivate = kf.generatePrivate(keySpec); Cipher cipher = Cipher.getInstance(ECB_PKCS1_PADDING); cipher.init(Cipher.ENCRYPT_MODE, keyPrivate); return cipher.doFinal(data); } catch (Exception e) { logger.error(e); } return null; } /** * 公鑰解密 * * @param data待解密資料 * @param publicKey 金鑰 * @return 解密後的資料 */ public static byte[] decryptByPublicKey(byte[] data, byte[] publicKey) { X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey); try { KeyFactory kf = KeyFactory.getInstance(RSA); PublicKey keyPublic = kf.generatePublic(keySpec); Cipher cipher = Cipher.getInstance(ECB_PKCS1_PADDING); cipher.init(Cipher.DECRYPT_MODE, keyPublic); return cipher.doFinal(data); } catch (Exception e) { logger.error(e); } return null; } /** * 私鑰解密 * * @param data待解密的資料 * @param privateKey 私鑰 * @return 解密後的資料 */ public static byte[] decryptByPrivateKey(byte[] data, byte[] privateKey) { PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKey); try { KeyFactory kf = KeyFactory.getInstance(RSA); PrivateKey keyPrivate = kf.generatePrivate(keySpec); Cipher cipher = Cipher.getInstance(ECB_PKCS1_PADDING); cipher.init(Cipher.DECRYPT_MODE, keyPrivate); return cipher.doFinal(data); } catch (Exception e) { logger.error(e); } return null; }
金鑰對生成後是一串灰常長特別長的資料,大家可以對其 Base64 後看看公鑰和隨機性如何。使用測試資料 123456abcdef
加密過、Base64 編碼後的結果如下。加密這麼短的字串竟然生成這麼長的密文,難怪 RSA 演算法這麼慢!
密文 Base64 後:X6yx1XfkVk4DZpnzcCSZr2oK+WMP7Azm4fBcGNEwWPrRdtf9isfMeKgQsI6kqOF5Vb5b5IYAIqHZRE5QcDbIM/3bTWVTVg/t7enGCUSxValIvJ/A37syWTUXlh59DZzBMgzG4rbziGCc8CGyO03XFq8gCncr4NMZXQwkKI8Alds=
一般來說,客戶端和服務端的通訊過程是這樣的:服務端生成 RSA 加密的金鑰對,把公鑰給客戶端,私鑰偷偷保留。客戶端在首次使用公鑰加密資料,然後傳送給服務端。服務端接收並處理後,會把對稱加密的金鑰下發給客戶端。客戶端接收到對稱加密的金鑰,以後的通訊就會使用對稱加密的方式。當然也可以由客戶端生成對稱加密的金鑰,然後用公鑰加密發給服務端。這樣在雙方交換金鑰時保證了安全,之後的對稱加密保證了效率。
好了,關於 Android 加密和編碼的方式就先介紹這麼多,歡迎大家留言交流~
【附錄】

資料圖