需求
現(xiàn)在大多數(shù)的手機(jī)客戶端采用Http+Json的方式與服務(wù)器進(jìn)行通信,也有采用TCP+ProtoBuf實(shí)現(xiàn),前者實(shí)現(xiàn)方便各種框架簡單的就構(gòu)建起了網(wǎng)絡(luò)通信模塊,后者的話需要強(qiáng)大的服務(wù)器性能,需要一定的技術(shù)儲備,ProtoBuf基于二進(jìn)制減少了傳輸?shù)膬?nèi)容但不易閱讀。我們使用抓包工具可以輕而易舉的抓取Http傳輸?shù)臄?shù)據(jù),有些競品公司會通過抓包爬蟲的方法抓取數(shù)據(jù)為己用,為防止這種情況,我們可以對數(shù)據(jù)加密。當(dāng)然也能采用Https的方式通信,但是Https較低的傳輸效率,更大的耗電量,需要花錢購買SSL證書等缺點(diǎn)不是一個最佳的選擇。本文采用的是對通信內(nèi)容的加解密達(dá)到數(shù)據(jù)保護(hù)的效果。
準(zhǔn)備
我們需要一種可逆的加密算法:AES加密,Android中對應(yīng)的是這個類:Cipher,AES需要一些加密模式等的配置,想要了解的同學(xué)可以看一下這篇文章,不過也無關(guān)緊要
對稱加密和分組加密中的四種模式(ECB、CBC、CFB、OFB)
另外我們需要一個校驗算法:CRC校驗,Android中對應(yīng)的是這個類:CRC32
AES加解密需要的password等是我們通過Https從服務(wù)器拉取,有關(guān)Https的配置使用,請參考這篇博客:
思路
Http里未做加密處理的body就是我們需要的Json數(shù)據(jù)的byte數(shù)組,現(xiàn)在需要對他的結(jié)構(gòu)重新定義。重新定義的body結(jié)構(gòu)包含三部分內(nèi)容,一個頭部分的字符串,這個頭部分是和后端統(tǒng)一的一個亂碼,主要是用來混淆視聽的,沒有特別的意義,簡稱為PRE,中間的這部分就是我們實(shí)際需要的json數(shù)據(jù),簡稱為CONTENT,這部分內(nèi)容是我們最終需要的數(shù)據(jù),也是別人最想得到的,最后的一部分是一個CRC32的校驗碼,他是對PRE+CONTENT進(jìn)行CRC校驗得到的數(shù)據(jù)。為什么需要這個CRC校驗碼呢?是這樣的,為了進(jìn)一步增強(qiáng)數(shù)據(jù)的安全性,AES需要的password是定期更新的,當(dāng)客戶端拿到PRE+CONTENT這部分內(nèi)容時會對這部分內(nèi)容做一次CRC校驗,得到的CRC碼會與服務(wù)器返回的CRC32做比較,這兩個不相等說明客戶端與服務(wù)器端使用了不同的password加解密,這時客戶端就要重新拉取最新的password并保存下來使用。舉個栗子:你昨天晚上打開了app,拿到了password為123一切使用正常,第二天你有打開了app,在這之前服務(wù)器更換了password為456,你在未知的情況下使用123繼續(xù)加解密,這時你們的crc碼就會對不上,所以要重新拉取最新的password,這就是CRC32這部分的用處,實(shí)際就是一個雙向校驗的過程。
代碼
首先先看Cipher這個類,它的init方法是這樣的
<pre>
Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");}
參數(shù)對應(yīng)的含義:"algorithm/mode/padding"
</pre>
所需參數(shù)需要和服務(wù)器端統(tǒng)一,這里我使用的是:
<pre>
private static final String CipherMode = "AES/CFB8/NoPadding";
</pre>
對于Http的get請求客戶端要做的是對數(shù)據(jù)的解密:
<pre>
private static byte[] decrypt(byte[] content, String password, String iv) {
try {
SecretKeySpec key = createKey(password);
Cipher cipher = Cipher.getInstance(CipherMode);
cipher.init(Cipher.DECRYPT_MODE, key, createIV(iv));
byte[] result = cipher.doFinal(content);
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
</pre>
所需的參數(shù) password,iv就是通過https獲取的AES加密所需password與iv。
這里還需要兩個字符串轉(zhuǎn)SecretKeySpec,IvParameterSpec的方法:
<pre>
private static SecretKeySpec createKey(String key) {
byte[] data = null;
if (key == null) {
key = "";
}
StringBuffer sb = new StringBuffer(16);
sb.append(key);
while (sb.length() < 16) {
sb.append("0");
}
if (sb.length() > 16) {
sb.setLength(16);
}
try {
data = sb.toString().getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return new SecretKeySpec(data, "AES");
}
private static IvParameterSpec createIV(String password) {
byte[] data = null;
if (password == null) {
password = "";
}
StringBuffer sb = new StringBuffer(16);
sb.append(password);
while (sb.length() < 16) {
sb.append("0");
}
if (sb.length() > 16) {
sb.setLength(16);
}
try {
data = sb.toString().getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return new IvParameterSpec(data);
}
</pre>
接下來的代碼就是按照思路進(jìn)行CRC的校驗,判斷password,iv是否過期,解密是否成功。
<pre>
public static byte[] aesdeData(byte[] data,String key,String iv) {
byte[] original = decrypt(data, key, iv);
byte[] crc1 = Arrays.copyOfRange(original, original.length - 4, original.length);
byte[] crc2 = new byte[8];
for (int i=0;i<4;i++){
crc2[i] = crc1[i];
}
ByteBuffer bf = ByteBuffer.wrap(crc2);
bf = bf.order(ByteOrder.LITTLE_ENDIAN);
byte[] temp = Arrays.copyOfRange(original, 0, original.length - 4);
String result = new String(temp);
byte[] datas = null;
if (bf.getLong()==crc(temp)){
result = result.replaceAll(PRE,"");
datas = result.getBytes();
}else {
//key 失效
result = null;
datas = null;
}
return datas;
}
</pre>
代碼很清晰,先把原始數(shù)據(jù)做了一次解密,然后區(qū)后32位的CRC碼,這個就是服務(wù)器對PRE+CONTENT的CRC校驗的結(jié)果,然后我們對除掉后32位的數(shù)據(jù)進(jìn)行CRC校驗,這相當(dāng)于客戶端對PRE+CONTENT的CRC校驗,兩次校驗的結(jié)果相等證明是一次成功的解密,這時就可以難道PRE+CONTENT的內(nèi)容了,不要忘記去掉PRE部分才是我們真正需要的Json。如果CRC的結(jié)果不等證明key iv失效了需要重新獲取。
對于Http的post請求客戶端要做的是對數(shù)據(jù)的加密:
單純加密的操作:
<pre>
private static byte[] encrypt(byte[] content, String password, String iv) {
try {
SecretKeySpec key = createKey(password);
Cipher cipher = Cipher.getInstance(CipherMode);
cipher.init(Cipher.ENCRYPT_MODE, key, createIV(iv));
byte[] result = cipher.doFinal(content);
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
</pre>
按照之前思路的加密:
<pre>
public static byte[] aesEn(String json, String key, String iv) {
String preContent = PRE + json;
long crc = crc(preContent);
byte[] preData = preContent.getBytes();// 前綴和body的 String -> byte[]
byte[] crcByte = longToByte(crc);//crc 64位 byte[]
byte[] crcData = getCrcData(crcByte);//crc 32位byte[]
byte[] finalData = byteMerger(preData, crcData);//最終需要加密的 byte[]
byte[] enData = encrypt(finalData, key, iv);
return enData;
}
</pre>
代碼很清晰就是對解密的逆操作
完整的代碼
可能遇到的問題
網(wǎng)絡(luò)序字節(jié)序的問題,與服務(wù)器統(tǒng)一
password iv失效造成加解密錯誤的情況,比較好的體驗是用戶無感知的情況下重新拉取password與iv并重復(fù)用戶之前的請求,筆者配合的是Volley實(shí)現(xiàn),不同框架的思路一致,這里不再講解。
END
七夕快樂
