本篇文章已受權微信公衆號 dasuAndroidTv(大蘇)獨家發佈html
此次想來說講網絡安全通訊這一塊,也就是網絡層封裝的那一套加密、解密,編碼、解碼的規則,不會很深刻,但會大概將這一整塊的講一講。java
之因此想寫這篇,是由於,最近被抽過去幫忙作一個 C++ 項目,在 Android 中,各類編解碼、加解密算法官方都已經封裝好了,咱們要使用很是的方便,但在 C++ 項目中不少都要本身寫。c++
然而,本身寫是不可能的了,沒這麼牛逼也沒這麼多時間去研究這些算法,網上天然不缺乏別人寫好的現成算法。但不一樣項目應用場景天然不同,通常來講,都須要對其進行修修改改才能拿到項目中來用。算法
踩的坑實在有點兒多,因此想寫一篇來總結一下。好了,廢話結束,開始正文。api
Q1: 你的 app 與後臺各接口通訊時有作身份校驗嗎?數組
Q2: 你的 app 與後臺各接口通訊的數據有涉及敏感數據嗎?你是如何處理的?安全
Q3: MD5 瞭解過嗎?微信
Q4: AES(16位密鑰 + CBC + PKCS5Padding) 呢?網絡
Q5: BASE64 呢?或者 UTF-8?app
第一點:爲何須要身份校驗?
身份校驗是作什麼,其實也就是校驗訪問接口的用戶合法性。說得白一點,也就是要過濾掉那些經過腳本或其餘非正常 app 發起的訪問請求。
試想一下,若是有人破解了服務端某個接口,而後寫個腳本,模擬接口所需的各類參數,這樣它就能夠假裝成正經常使用戶從這個接口拿到他想要的數據了。
更嚴重點的是,若是他想圖摸不軌,向服務端發送了一堆僞造的數據,若是這些數據會對服務端形成損失怎麼辦。
因此,基本上服務端的接口都會有身份校驗機制,來檢測訪問的對象是否合法。
第二點:MD5 算法是什麼?
通俗的講,MD5 算法能對一串輸入生成一串惟一的不可逆的 128 bit 的 0 和 1 的二進制串信息。
一般 app 都會在發起請求前根據本身公司所定義的規則作一次 MD5 計算,做爲 token 發送給服務端進行校驗。
MD5 有兩個特性:惟一性和不可逆性。
惟一性能夠達到防止輸入被篡改的目的,由於一旦第三方攻擊者劫持了這個請求,篡改了攜帶的參數,那麼服務端只要再次對這些輸入作一次 MD5 運算,比較計算的結果與 app 上傳的 token 便可檢測出輸入是否有被修改。
不可逆的特色,則是就算第三方攻擊者劫持了此次請求,看到了攜帶的參數,以及 MD5 計算後的 token,那麼他也沒法從這串 token 反推出咱們計算 MD5 的規則,天然也就沒法僞造新的 token,那麼也就沒法經過服務端的校驗了。
第三點:理解 16 位和 32 位 MD5 值的區別
網上有不少在線進行 MD5 計算的工具,如 http://www.cmd5.com/,這裏演示一下,嘗試一下分別輸入:
I am dasu
和 I'm dasu
看一下通過 MD5 運算後的結果:
首先確認一點,不一樣的輸入,輸出就會不同,即便只作了細微修改,二者輸出仍舊毫無規律而言。
另外,由於通過 MD5 計算後輸出是 128 bit 的 0 和 1 二進制串,但一般都是用十六進制來表示比較友好,1個十六進制是 4 個 bit,128 / 4 = 32,因此常說的 32 位的 MD5 指的是用十六進制來表示的輸出串。
那麼,爲何還會有 16 位的 MD5 值?其實也就是嫌 32 位的數據太長了,因此去掉開頭 8 位,末尾 8 位,截取中間的 16 位來做爲 MD5 的輸出值。
因此,MD5 算法的輸出只有一種:128 bit 的二進制串,而一般結果都用十六進制表示而已,32 位與 16 位的只是精度的區別而已。
第四點:MD5 的應用
應用場景不少:數字簽名、身份校驗、完整性(一致性)校驗等等。
這裏來說講 app 和服務端接口訪問經過 MD5 來達到身份校驗的場景。
app 持有一串密鑰,這串密鑰服務端也持有,除此外別人都不知道,所以 app 就能夠跟服務端協商,兩邊統一下交互的時候都有哪些數據是須要加入 MD5 計算的,以怎樣的規則拼接進行 MD5 運算的,這樣一旦這些數據被三方攻擊者篡改了,也能檢查出來。
也就是說,密鑰和拼接規則都是關鍵點,不能夠泄漏出去。
MD5 只能達到校驗的目的,而 app 與服務端交互時,數據都是在網絡中傳輸的,這些請求若是被三方劫持了,那麼若是交互的數據裏有一些敏感信息,就會遭到泄漏,存在安全問題。
固然,若是你的 app 與服務端的交互都是 HTTPS 協議了的話,那麼天然就是安全的,別人抓不到包,也看不到信息。
若是仍是基於 HTTP 協議的話,那麼有不少工具均可以劫持到這個 HTTP 包,app 與服務端交互的信息就這樣赤裸裸的展現在別人面前。
因此,一般一些敏感信息都會通過加密後再發送,接收方拿到數據後再進行解密便可。
而加解密的世界很複雜,對稱加密、非對稱加密,每一種類型的加解密算法又有不少種,不展開了,由於實在展開不了,我門檻都沒踏進去,實在沒去深刻學習過,目前只大概知道個流程原理,會用的程度。
那麼,本篇就介紹一種網上很常見的一整套加解密、編解碼流程:
UTF-8 和 BASE64 都屬於編解碼,AES 屬於對稱加密算法。
信息其實本質上是由二進制串組成,經過各類不一樣的編碼格式,來將這段二進制串信息解析成具體的數據。好比 ASCII 編碼定義了一套標準的英文、常見符號、數字的編碼;UTF-8 則是支持中文的編碼。目前大部分的 app 所使用的數據都是基於 UTF-8 格式的編碼的吧。
AES 屬於對稱加密算法,對稱的意思是說,加密方和解密方用的是同一串密鑰。信息通過加密後會變成一串毫無規律的二進制串,此時再選擇一種編碼方式來展現,一般是 BASE64 格式的編碼。
BASE64 編碼是將全部信息都編碼成只用大小寫字母、0-9數字以及 + 和 / 64個字符表示,全部稱做 BASE64。
不一樣的編碼所應用的場景不一樣,好比 UTF-8 傾向於在終端上呈現各類複雜字符包括簡體、繁體中文、日文、韓文等等數據時所使用的一種編碼格式。而 BASE64 編碼一般用於在網絡中傳輸較長的信息時所使用的一種編碼格式。
基於以上種種,目前較爲常見的 app 與服務端交互的一套加解密、編解碼流程就是:UTF-8 + AES + BASE64
上圖就是從 app 端發數據給服務端的一個加解密、編解碼過程。
須要注意的是,由於 AES 加解密時輸入和輸出都是二進制串的信息,所以,在發送時需先將明文經過 UTF-8 解碼成二進制串,而後進行加密,再對這串二進制密文經過 BASE64 編碼成密文串發送給接收方。
接收方的流程就是反着來一遍就對了。
理論上基本清楚了,那麼接下去就是代碼實現了,Android 項目中要實現很簡單,由於 JDK 和 SDK 中都已經將這些算法封裝好了,直接調用 api 接口就能夠了。
public class EncryptDecryptUtils { private static final String ENCODE = "UTF-8"; //AES算法加解密模式有多種,這裏選擇 CBC + PKCS5Padding 模式,CBC 須要一個AES_IV偏移量參數,而AES_KEY 是密鑰。固然,這裏都是隨便寫的,這些信息很關鍵,不宜泄露 private static final String AES = "AES"; private static final String AES_IV = "aaaaaaaaaaaaaaaa"; private static final String AES_KEY = "1111111111111111";//16字節,128bit,三種密鑰長度中的一種 private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; /** * AES加密後再Base64編碼,輸出密文。注意AES加密的輸入是二進制串,因此須要先將UTF-8明文轉成二進制串 */ public static String doEncryptEncode(String content) throws Exception { SecretKeySpec secretKeySpec = new SecretKeySpec(AES_KEY.getBytes(ENCODE), AES); Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, new IvParameterSpec(AES_IV.getBytes(ENCODE))); //1. 先獲取二進制串,再進行AES(CBC+PKCS5Padding)模式加密 byte[] result = cipher.doFinal(content.getBytes(ENCODE)); //2. 將二進制串編碼成BASE64串 return Base64.encodeToString(result, Base64.NO_WRAP); } /** * Base64解碼後再進行AES解密,最後對二進制明文串進行UTF-8編碼輸出明文串 */ public static String doDecodeDecrypt(String content) throws Exception { SecretKeySpec secretKeySpec = new SecretKeySpec(AES_KEY.getBytes(ENCODE), AES); Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(AES_IV.getBytes(ENCODE))); //1. 先將BASE64密文串解碼成二進制串 byte[] base64 = Base64.decode(content, Base64.NO_WRAP); //2. 再將二進制密文串進行AES(CBC+PKCS5Padding)模式解密 byte[] result = cipher.doFinal(base64); //3. 最後將二進制的明文串以UTF-8格式編碼成字符串後輸出 return new String(result, Charset.forName(ENCODE)); } }
Java 的實現代碼是否是很簡單,具體算法的實現都已經封裝好了,就是調一調 api 的事。
這裏須要稍微知道下,AES 加解密模式分不少種,首先,它有三種密鑰形式,分別是 128 bit,192 bit,256 bit,注意是 bit,Java 中的字符串每一位是 1B = 8 bit,因此上面例子中密鑰長度是 16 位的字符串。
除了密鑰外,AES 還分四種模式的加解密算法:ECB,CBC,CFB,OFB,這涉及到具體算法,我也不懂,就不介紹了,清楚上面是使用了 CBC 模式就能夠了。
最後一點,使用 CBC 模式進行加密時,是對明文串進行分組加密的,每組的大小都同樣,所以在分組時就有可能會存在最後一組的數量不夠的狀況,那麼這時就須要進行填充,而這個填充的概念就是 PKCS5Padding 和 PKCS7Padding 兩種。
這兩種的填充規則都同樣,具體可看其餘的文章,區別只在於分組時規定的每組的大小。在PKCS5Padding中,明肯定義 Block 的大小是 8 位,而在 PKCS7Padding 定義中,對於塊的大小是不肯定的,能夠在 1-255 之間。
稍微瞭解下這些就夠了,若是你不繼續往下研究 C++ 的寫法,這些不瞭解也沒事,會用就行。
c++ 坑爹的地方就在於,這整個流程,包括 UTF-8 編解碼、AES 加解密、BASE64 編解碼都得本身寫。
固然,不可能本身寫了,網上輪子那麼多了,但問題就在於,由於 AES 加解密模式太多了,網上的資料大部分都只是針對其中一種進行介紹,所以,若是不稍微瞭解一下相關原理的話,就無從下手進行修改了。
我這篇,天然也只是介紹我所使用的模式,若是你恰好跟我同樣,那也許能夠幫到你,若是跟你不同,至少我列出了資料的來源,整篇下來也稍微講了一些基礎性的原理,掌握這些,作點兒修修補補應該是能夠的。
貼代碼前,先將我所使用的模式列出來:
UTF-8 + AES(16位密鑰 + CBC + PKCS5Padding) + BASE64
其實這些都相似於工具類,官方庫沒提供,那網上找個輪子就行了,都是一個 h 和 cpp 文件而已,複製粘貼下就能夠了。重點在於準備好了這些工具類後,怎麼用,怎麼稍微修改。
若是你不想本身網上找,那下面我已經將相關連接都貼出來了,去複製粘貼下就能夠了。
我最開始就是拿的第二篇來用的,而後才發現他所採用的模式是:AES(16位密鑰 + CBC + PKCS7Padding) + BASE64
也就是說,他的例子中不支持中文的加解密,並且填充模式採用的是 PKCS7Padding,跟個人不一致。一開始我也不瞭解相關原理基礎,怎麼調都調不出結果,無奈只能先去學習下原理基礎。
還好後面慢慢的理解了,也懂得該改哪些地方,也增長了 UTF-8 編解碼的處理。下面貼的代碼中註釋會寫得很清楚,整篇看下來,我相信,就算你模式跟個人也不同,你的密鑰是24位的、32位的,不要緊,稍微改一改就能夠了。
//EncryptDecryptUtils.h #pragma once #include <string> using namespace std; #ifndef AES_INFO #define AES_INFO #define AES_KEY "1111111111111111" //AES 16B的密鑰 #define AES_IV "aaaaaaaaaaaaaaaa" //AES CBC加解密模式所需的偏移量 #endif class EncryptDecryptUtils { public: //解碼解密 static string doDecodeDecrypt(string content); //加密編碼 static string doEncryptEncode(string content); EncryptDecryptUtils(); ~EncryptDecryptUtils(); private: //去除字符串中的空格、換行符 static string removeSpace(string content); };
如下才是具體實現,其中在頭部 include 的 AES.h,Base64.h,UTF8.h 須要先從上面給的博客連接中將相關代碼複製粘貼過來。這些文件基本都是做爲工具類使用,不須要進行改動。可能須要稍微改一改的就只是 AES.h 文件,由於不一樣的填充模式須要改一個常量值。
//EncryptDecryptUtils.cpp #include "EncryptDecryptUtils.h" #include "AES.h" #include "Base64.h" #include "UTF8.h" EncryptDecryptUtils::EncryptDecryptUtils() { } ~EncryptDecryptUtils::EncryptDecryptUtils() { } /** * 流程:服務端下發的BASE64編碼的密文字符串 -> 去除字符串中的換行符 -> BASE64解碼 -> AES::CBC模式解密 -> 去掉AES::PKCS5Padding 填充 -> UTF-8編碼 -> 明文字符串 */ string EncryptDecryptUtils::doDecodeDecrypt(string content) { //1.去掉字符串中的\r\n換行符 string noWrapContent = removeSpace(string); //2. Base64解碼 string strData = base64_decode(noWrapContent); size_t length = strData.length(); //3. new些數組,給解密用 char *szDataIn = new char[length + 1]; memcpy(szDataIn, strData.c_str(), length + 1); char *szDataOut = new char[length + 1]; memcpy(szDataOut, strData.c_str(), length + 1); //4. 進行AES的CBC模式解密 AES aes; //在這裏傳入密鑰,和偏移量,以及指定密鑰長度和iv長度,若是你的密鑰長度不是16字節128bit,那麼須要在這裏傳入相對應的參數。 aes.MakeKey(string(AES_KEY).c_str(), string(AES_IV).c_str(), 16, 16); //這裏參數有傳入指定加解密的模式,AES::CBC,若是你不是這個模式,須要傳入相對應的模式,源碼中都有註釋說明 aes.Decrypt(szDataIn, szDataOut, length, AES::CBC); //5.去PKCS5Padding填充:解密後須要將字符串中填充的去掉,根據填充規則進行去除,感興趣可去搜索相關的填充規則 if (0x00 < szDataOut[length - 1] <= 0x16) { int tmp = szDataOut[length - 1]; for (int i = length - 1; i >= length - tmp; i--) { if (szDataOut[i] != tmp) { memset(szDataOut, 0, length); break; } else szDataOut[i] = 0; } } //6. 將二進制的明文串轉成UTF-8格式的編碼方式,輸出 string srcDest = UTF8_To_string(szDataOut); delete[] szDataIn; delete[] szDataOut; return srcDest; } /** * 流程:UTF-8格式的明文字符串 -> UTF-8解碼成二進制串 -> AES::PKCS5Padding 填充 -> AES::CBC模式加密 -> BASE64編碼 -> 密文字符串 */ string EncryptDecryptUtils::doEncryptEncode(string content) { //1. 先獲取UTF-8解碼後的二進制串 string utf8Content = string_To_UTF8(content); size_t length = utf8Content.length(); int block_num = length / BLOCK_SIZE + 1; //2. new 些數組供加解密使用 char* szDataIn = new char[block_num * BLOCK_SIZE + 1]; memset(szDataIn, 0x00, block_num * BLOCK_SIZE + 1); strcpy(szDataIn, utf8Content.c_str()); //3. 進行PKCS5Padding填充:進行CBC模式加密前,須要填充明文串,確保能夠分組後各組都有相同的大小。 // BLOCK_SIZE是在AES.h中定義的常量,PKCS5Padding 和 PKCS7Padding 的區別就是這個 BLOCK_SIZE 的大小,我用的PKCS5Padding,因此定義成 8。若是你是使用 PKCS7Padding,那麼就根據你服務端具體大小是在 1-255中的哪一個值修改便可。 int k = length % BLOCK_SIZE; int j = length / BLOCK_SIZE; int padding = BLOCK_SIZE - k; for (int i = 0; i < padding; i++) { szDataIn[j * BLOCK_SIZE + k + i] = padding; } szDataIn[block_num * BLOCK_SIZE] = '\0'; char *szDataOut = new char[block_num * BLOCK_SIZE + 1]; memset(szDataOut, 0, block_num * BLOCK_SIZE + 1); //4. 進行AES的CBC模式加密 AES aes; //在這裏傳入密鑰,和偏移量,以及指定密鑰長度和iv長度,若是你的密鑰長度不是16字節128bit,那麼須要在這裏傳入相對應的參數。 aes.MakeKey(string(AES_KEY).c_str(), string(AES_IV).c_str(), 16, 16); //這裏參數有傳入指定加解密的模式,AES::CBC,若是你不是這個模式,須要傳入相對應的模式,源碼中都有註釋說明 aes.Encrypt(szDataIn, szDataOut, block_num * BLOCK_SIZE, AES::CBC); //5. Base64編碼 string str = base64_encode((unsigned char*)szDataOut, block_num * BLOCK_SIZE); delete[] szDataIn; delete[] szDataOut; return str; } //去除字符串中的空格、換行符 string EncryptDecryptUtils::formatText(string src) { int len = src.length(); char *dst = new char[len + 1]; int i = -1, j = 0; while (src[++i]) { switch (src[i]) { case '\n': case '\t': case '\r': continue; } dst[j++] = src[i]; } dst[j] = '\0'; string rel = string(dst); delete dst; return rel; }
再列個在線驗證 AES 加解密結果的網站,方便調試:
http://www.seacha.com/tools/aes.html
Java 實現那麼方便,爲何還須要用 C++ 的呢?
想想,密鑰信息那麼重要,你要放在哪?像我例子那樣直接寫在代碼中?那只是個例子,別忘了,app 混淆的時候,字符串都是不會參與混淆的,隨便反編譯下你的 app,密鑰就暴露給別人了。
那麼,有其餘比較好的方式嗎?我只能想到,AES 加解密相關的用 C++ 來寫,生成個 so 庫,提供個 jni 接口給 app 層調用,這樣密鑰信息就能夠保存在 C++ 中了。
也許你會以爲,哪有人那麼閒去反編譯 app,並且正在寫的 app 又沒有什麼價值讓別人反編譯。
emmm,說是這麼說,但安全意識仍是要有的,至少也要先知道有這麼個防禦的方法,以及該怎麼作,萬一哪天你寫的 app 就火了呢?
你們好,我是 dasu,歡迎關注個人公衆號(dasuAndroidTv),若是你以爲本篇內容有幫助到你,能夠轉載但記得要關注,要標明原文哦,謝謝支持~