文章目錄
在Android直播開發之旅(14):使用RC4算法加解密音視頻流一文中,我們瞭解了一種面向字節操作的對稱加密算法–RC4,該算法實現簡潔、加密速度快且安全性較高(
密鑰可變長,爲1~256字節
),通常用於加密流數據。本節將介紹另外一種對稱加密–AES,通過分析它的實現原理並結合實戰項目來了解其在文件加密方面的應用。
1. AES算法
高級加密標準(英語:Advanced Encryption Standard,縮寫:AES),在密碼學中又稱Rijndael加密法,是美國聯邦政府採用的一種區塊加密標準。AES 是一個迭代的、對稱密鑰分組的密碼,AES算法加密強度大,執行效率高,使用簡單。這個標準用來替代原先的DES,已經被多方分析且廣爲全世界所使用,是目前對稱密鑰加密中最流行的算法之一。AES的分組區塊長度
固定爲128位,密鑰長度
則可以是128,192或256位。密鑰長度越長,加密等級越高,但是效率會有所降低。AES算法常見參數配置:
參數類型 | AES128 | AES192 | AES256 |
---|---|---|---|
密碼長度(bits) | 128 | 192 | 256 |
明文分組長度(bits) | 128 | 128 | 128 |
加密輪數 | 10 | 12 | 14 |
輪密鑰長度(bits) | 128 | 128 | 128 |
擴展密鑰長度(words) | 44 | 52 | 60 |
AES算法在對明文加密的時候,並不是把整個明文一股腦加密成一整段密文,而是把明文拆分成一個個獨立的明文塊(分組),每一個明文塊長度128bit=16字節。這些明文塊經過AES加密器的複雜處理,生成一個個獨立的密文塊,這些密文塊拼接在一起,就是最終的AES加密結果。本文將以AES128爲例,講解AES的實現過程。
1.1 AES加密過程
AES加密過程是在一個4×4的字節(即16字節)矩陣
上運作,這個矩陣又稱爲“體(state)”,其初值就是一個明文區塊(矩陣中一個元素大小就是明文區塊中的一個Byte)。加密時,各輪AES加密分爲初始輪(Initial Round,1次)、普通輪(Rounds,N次)和最終輪(Final Rounds,1次),並且不同階段的Round由多同的處理步驟,整個AES加密過程如下圖所示(以AES128爲例
):
其中,SubBytes
表示字節代替變換、ShiftRows
表示行移位變換、MixColumns
表示列混合變換、AddRoundKey
表示加輪密鑰變換。
1.1.1 字節代替(SubBytes)
所謂字節替換,是指將明文塊(16字節組成的4x4矩陣)中的每一個字節都替換成另外一個字節,其中替換的依據是一個被稱爲S盒(Subtitution Box)的16x16大小的二維常量數組。比如明文塊當中a[2, 2] = 19
,那麼經過被S盒替換後輸出的目標b[2, 2] = S[1][9]=2D
,即a[2,2]的值被替換成S矩陣中行下標爲1、列下標爲9的元素。
1.1.2 行移位(ShiftRows)
所謂行移位,是指對進行字節替換後的矩陣進行行移位,其規則如下:
- 第一行不變;
- 第二行循環左移1個字節;
- 第三行循環左移2個字節;
- 第三行循環左移3個字節;
1.1.3 列混合(MixColumns)
所謂列混合,是指將經過行移位得到的矩陣將其每一列和一個名爲修補矩陣(fixed matrix)
的二維常量數組做矩陣相乘,得到對應的輸出列。舉例如下:
1.1.4 加輪密鑰(AddRoundKey)
所謂加輪密鑰,是指將輸入的數組的每一個字節a[i,j]
與密鑰對應位置的字節k[i,j]
進行異或依次,從而得到處理後的輸出值b[i,j]
。這時唯一利用到密鑰的一步,且128bit的密鑰也同樣被排列成4x4的矩陣。需要注意的是,由於加密的每一輪所用到的密鑰並不是相同的,而每一輪的密鑰通過**擴展密鑰(KeyExpansions)**產生,擴展密鑰算法計算過程爲:
注:關於AES的加解密過程,可參照《A高級加密標準AES 徐娟》和《AES加密算法動畫》來理解。另外,還有一個知識點就是
AES
的工作模式,AES加密算法提供了五種不同的工作模式:ECB、CBC、CTR、CFB、OFB,其中,ECB模式爲默認工作模式,這種模式的每一個明文塊的加密都是完全獨立的,互不干涉的,因此實現起來比較簡單且有利於並行計劃,但是安全性稍微差一點。如果希望提高安全性,可以採用CBC模式,該模式在每一個明文塊加密前會讓明文塊和一個值先做異或操作,但是缺點是無法並行計算,且由於增加了異或操作,性能上不如ECB模式。
1.2 AES加解密實現
1.2.1 創建密鑰
public static byte[] getAutoCreateAESKey() throws Exception {
// 實例化一個AES加密算法的密鑰生成器
KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM_AES);
// 初始化密鑰生成器,指定密鑰位數爲128位
keyGenerator.init(AES_KEY_LEN, new SecureRandom());
// 生成一個密鑰
SecretKey secretKey = keyGenerator.generateKey();
return secretKey.getEncoded();
}
1.2.2 AES加密
/** AES加密
*
* @param sourceFile 源文件
* @param encryptFile 加密文件
* @param password 密鑰,128bit
* @throws Exception 拋出異常
*/
public static void aesEncryptFile(String sourceFile, String encryptFile, byte[] password) throws Exception {
// 創建AES密鑰
SecretKeySpec key = new SecretKeySpec(password, "AES");
// 創建加密引擎(CBC模式)。Cipher類支持DES,DES3,AES和RSA加加密
// AES:算法名稱
// CBC:工作模式
// PKCS5Padding:明文塊不滿足128bits時填充方式(默認),即在明文塊末尾補足相應數量的字符,
// 且每個字節的值等於缺少的字符數。另外一種方式是ISO10126Padding,除最後一個字符值等於少的字符數
// 其他字符填充隨機數。
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// 初始化加密器
cipher.init(Cipher.ENCRYPT_MODE, key,
new IvParameterSpec(new byte[cipher.getBlockSize()]));
// 原始文件流
FileInputStream inputStream = new FileInputStream(sourceFile);
// 加密文件流
FileOutputStream outputStream = new FileOutputStream(encryptFile);
// 以加密流寫入文件
CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher);
byte[] tmpArray = new byte[1024];
int len;
while((len = cipherInputStream.read(tmpArray)) != -1) {
outputStream.write(tmpArray, 0, len);
outputStream.flush();
}
cipherInputStream.close();
inputStream.close();
outputStream.close();
}
1.2.3 AES解密
/** AES解密
*
* @param encryptFile 加密文件
* @param decryptFile 解密文件
* @param password 密鑰,128bit
* @throws Exception 拋出異常
*/
public static void aesDecryptFile(String encryptFile, String decryptFile, byte[] password) throws Exception {
// 創建AES密鑰,即根據一個字節數組構造一個SecreteKey
// 而這個SecreteKey是符合指定加密算法密鑰規範
SecretKeySpec key = new SecretKeySpec(password, "AES");
// 創建解密引擎(CBC模式)
// Cipher類支持DES,DES3,AES和RSA加解密
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// 初始化解密器
cipher.init(Cipher.DECRYPT_MODE, key,
new IvParameterSpec(new byte[cipher.getBlockSize()]));
// 加密文件流
FileInputStream fileInputStream = new FileInputStream(encryptFile);
// 解密文件流
FileOutputStream fileOutputStream = new FileOutputStream(decryptFile);
// 以解密流寫出文件
CipherOutputStream cipherOutputStream =
new CipherOutputStream(fileOutputStream, cipher);
byte[] buffer = new byte[1024];
int len;
while((len = fileInputStream.read(buffer)) >= 0) {
cipherOutputStream.write(buffer, 0, len);
}
cipherOutputStream.close();
fileInputStream.close();
fileOutputStream.close();
}
1.3 AES與DES的對比
AES、DES均爲對稱加密算法,它們的對比如下:
DES | AES | |
---|---|---|
安全性 | 密鑰長度較短(56bits),不能抵抗密鑰的窮舉搜索攻擊;輪密鑰通過置換和循環移位進行擴展,存在弱密鑰和半弱密鑰 | 密鑰長度至少128bits,安全性能超過3-DES;輪密鑰通過非線性代換、線性混合和密鑰加進行擴展 |
效率 | 快 | 快於3-DES |
靈活性 | 密鑰長度不可變(56bits) | 密鑰長度可變(128bits、192bits、256bits) |
複雜性 | 採用Feistel網絡結構,每輪數據的變換不均勻,設計較爲複雜 | 採用SP網絡結構,每輪數據的變換均勻,設計簡單 |
2. RSA算法
RSA加密算法是一種非對稱加密算法
,該算法在公開密鑰加密和電子商業中被廣泛使用,只要其鑰匙長度足夠長(注:至少爲500位長,推薦使用1024位
),世界上還沒有任何可靠的攻擊RSA算法的方式。所謂非對稱加密算法,是指算法的加解密需要兩個(一對)密鑰,即公開密鑰(publickey,簡稱公鑰
)和私有密鑰(privatekey,簡稱私鑰
),其中公鑰通常用於對數據進行加密且公開,而私鑰用於對數據進行解密且私有。
2.1 RSA算法原理
RSA算法基於一個十分簡單的數論事實:將兩個大素數相乘十分容易,但是想要對其乘積進行因式分解卻極其困難,因此可以將乘積公開作爲加密密鑰
。在RSA算法加解密過程中,假設用戶A生成一對RSA密鑰,其中私鑰由A自己保管,公鑰將對外公開。用戶B獲得A公佈的公鑰後,它拿着公鑰對數據進行加密然後傳輸給A,A獲得B傳輸過來的加密數據後,就用自己保存的私鑰進行解密,進而獲得明文。整個加解密過程如下圖所示:
1. RSA密鑰對生成流程
(1)隨意選擇兩個大的素數p、q
;
(2)計算兩個大素數的乘機n = p * q
;
(3)φ(n) = (p-1)(q-1)
(歐拉函數);
(4)公鑰e: 滿足 1<e<φ(n)
的整數 且和 φ(n)互質;
(5)私鑰d: 滿足e*d mod φ(n)==1
;
(6)銷燬p, q;
(7)公開發布n和公鑰e;
2. 加、解密
(1)加密:假設明文m(m應爲小於n的整數),m的e次冪取n的餘數,得到密文c。
(2)解密:計算密文c的d次冪取n的餘數,即得到明文m。
2.2 RSA加解密實現
2.2.1 創建RSA密鑰對
private static Map<Integer, String> keyMap = new HashMap<Integer, String>();
/**
* 隨機生成密鑰對
* @throws NoSuchAlgorithmException
*/
public static void genKeyPair() throws NoSuchAlgorithmException {
// KeyPairGenerator類用於生成公鑰和私鑰對,基於RSA算法生成對象
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
// 初始化密鑰對生成器,密鑰大小爲96-1024位
keyPairGen.initialize(1024,new SecureRandom());
// 生成一個密鑰對,保存在keyPair中
KeyPair keyPair = keyPairGen.generateKeyPair();
// 得到私鑰
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// 得到公鑰
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
// 將公鑰和私鑰保存到Map
// 0表示公鑰
// 1表示私鑰
keyMap.put(0,Base64.getEncoder().encodeToString(publicKey.getEncoded()));
keyMap.put(1,Base64.getEncoder().encodeToString(privateKey.getEncoded()));
}
2.2.1 RSA加密
/**
* RSA公鑰加密
*
* @param str
* 加密字符串
* @param publicKey
* 公鑰
* @return 密文,經過Base64處理
* @throws Exception
* 加密過程中的異常信息
*/
public static String rsaEncrypt(String str, String publicKey ) throws Exception{
// 獲取公鑰
byte[] decoded = Base64.getDecoder().decode(publicKey);
RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(decoded));
//RSA加密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
return Base64.getEncoder().encodeToString((cipher.doFinal(str.getBytes("UTF-8")));
}
2.2.2 RSA解密
/**
* RSA私鑰解密
*
* @param str
* 加密字符串
* @param privateKey
* 私鑰
* @return 銘文
* @throws Exception
* 解密過程中的異常信息
*/
public static String rsaDecrypt(String str, String privateKey) throws Exception{
//64位解碼加密後的字符串
byte[] inputByte = Base64.getDecoder().decode(str.getBytes("UTF-8"));
// base64編碼的私鑰
byte[] decoded = Base64.getDecoder().decode(privateKey);
RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA")
.generatePrivate(new PKCS8EncodedKeySpec(decoded));
//RSA解密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, priKey);
return new String(cipher.doFinal(inputByte));
}
3. MD5算法
MD5信息摘要算法(MD5 Message-Digest Algorithm),一種被廣泛使用的密碼散列函數,可以產生出一個128位(16字節)的散列值(hash value),用於確保信息(文件)傳輸完整一致。MD5能把一個不管多長的文件都變成定長的且是不可逆的,它的實現原理大致爲:MD5碼以512位分組來處理輸入的信息,且每一分組又被劃分爲16個32位子分組,經過了一系列的處理後,算法的輸出由四個32位分組組成,將這四個32位分組級聯後將生成一個128位散列值。
總體流程如下圖所示:
代碼實現:
/** 計算文件的md5值 */
public static String calculateMD5(File updateFile, int offset, int partSize) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
Log.e("FileUtils", "Exception while getting digest", e);
return null;
}
InputStream is;
try {
is = new FileInputStream(updateFile);
} catch (FileNotFoundException e) {
Log.e("FileUtils", "Exception while getting FileInputStream", e);
return null;
}
//DigestInputStream
final int buffSize = 8192;//單塊大小
byte[] buffer = new byte[buffSize];
int read;
try {
if (offset > 0) {
is.skip(offset);
}
int byteCount = Math.min(buffSize, partSize), byteLen = 0;
while ((read = is.read(buffer, 0, byteCount)) > 0 && byteLen < partSize) {
digest.update(buffer, 0, read);
byteLen += read;
//檢測最後一塊,避免多讀數據
if (byteLen + buffSize > partSize) {
byteCount = partSize - byteLen;
}
}
byte[] md5sum = digest.digest();
BigInteger bigInt = new BigInteger(1, md5sum);
String output = bigInt.toString(16);
// Fill to 32 chars
output = String.format("%32s", output).replace(' ', '0');
return output;
} catch (IOException e) {
throw new RuntimeException("Unable to process file for MD5", e);
} finally {
try {
is.close();
} catch (IOException e) {
Log.e("FileUtils", "Exception on closing MD5 input stream", e);
}
}
}
4. Base64算法
Bse64是一種以64個可見字符集(又稱“Base64編碼表”
)對二進制數據進行編碼的編碼算法,這種編碼不僅比較簡短,同時也具有不可讀性,即所編碼的數據不會被人用肉眼所直接看到,因此常用於電子郵件加密
、數據簡單加密
以及圖片和文件網絡傳輸
。Base64編碼表如下圖所示:
(1)Base64編碼過程
在Base64編碼過程中,每3個8位明文數據爲一組(注:即每3個字節爲一組
),取這3個字數據的ASCII碼,然後以6位爲一組組成1個新的數據。對於不足3字節的處理:(1)不足三字節後面填充0;(2)對於編碼前的數據產生的6位,如果爲0,則索引到的字符爲‘A’;因不足3字節而填充的0,用’=’來替代。舉例如下:
由此可知,“ABCD
”的base64編碼爲:“QUJDRA==
”。代碼實現:
Base64.getEncoder().encode("ABCD".getBytes());
(2)Base64解碼過程
Base64解碼,即是base64編碼的逆過程,即將base64編碼數據根據編碼表分別索引到編碼值,然後每4個編碼值一組組成一個24位的數據流,解碼爲3個字符(注:每個字符佔8位
)。對於末尾位“=”的base64數據,最終取得的4字節數據,需要去掉“=”再進行轉換。代碼實現:
Base64.getDecoder().decode("QUJDRA==");