講講網絡模塊中加解密那點兒事--AES+BASE64

本篇文章已受權微信公衆號 dasuAndroidTv(大蘇)獨家發佈html

此次想來說講網絡安全通訊這一塊,也就是網絡層封裝的那一套加密、解密,編碼、解碼的規則,不會很深刻,但會大概將這一整塊的講一講。java

之因此想寫這篇,是由於,最近被抽過去幫忙作一個 C++ 項目,在 Android 中,各類編解碼、加解密算法官方都已經封裝好了,咱們要使用很是的方便,但在 C++ 項目中不少都要本身寫。c++

然而,本身寫是不可能的了,沒這麼牛逼也沒這麼多時間去研究這些算法,網上天然不缺乏別人寫好的現成算法。但不一樣項目應用場景天然不同,通常來講,都須要對其進行修修改改才能拿到項目中來用。算法

踩的坑實在有點兒多,因此想寫一篇來總結一下。好了,廢話結束,開始正文。api

提問

Q1: 你的 app 與後臺各接口通訊時有作身份校驗嗎?數組

Q2: 你的 app 與後臺各接口通訊的數據有涉及敏感數據嗎?你是如何處理的?安全

Q3: MD5 瞭解過嗎?微信

Q4: AES(16位密鑰 + CBC + PKCS5Padding) 呢?網絡

Q5: BASE64 呢?或者 UTF-8?app

理論

身份校驗 -- MD5 算法

第一點:爲何須要身份校驗?

身份校驗是作什麼,其實也就是校驗訪問接口的用戶合法性。說得白一點,也就是要過濾掉那些經過腳本或其餘非正常 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 dasuI'm dasu 看一下通過 MD5 運算後的結果:

MD5.png

MD5_.png

首先確認一點,不一樣的輸入,輸出就會不同,即便只作了細微修改,二者輸出仍舊毫無規律而言。

另外,由於通過 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 運算的,這樣一旦這些數據被三方攻擊者篡改了,也能檢查出來。

也就是說,密鑰和拼接規則都是關鍵點,不能夠泄漏出去。

敏感數據加密 -- AES + BASE64

MD5 只能達到校驗的目的,而 app 與服務端交互時,數據都是在網絡中傳輸的,這些請求若是被三方劫持了,那麼若是交互的數據裏有一些敏感信息,就會遭到泄漏,存在安全問題。

固然,若是你的 app 與服務端的交互都是 HTTPS 協議了的話,那麼天然就是安全的,別人抓不到包,也看不到信息。

若是仍是基於 HTTP 協議的話,那麼有不少工具均可以劫持到這個 HTTP 包,app 與服務端交互的信息就這樣赤裸裸的展現在別人面前。

因此,一般一些敏感信息都會通過加密後再發送,接收方拿到數據後再進行解密便可。

而加解密的世界很複雜,對稱加密、非對稱加密,每一種類型的加解密算法又有不少種,不展開了,由於實在展開不了,我門檻都沒踏進去,實在沒去深刻學習過,目前只大概知道個流程原理,會用的程度。

那麼,本篇就介紹一種網上很常見的一整套加解密、編解碼流程:

UTF-8 + AES + BASE64

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

加解密流程.png

上圖就是從 app 端發數據給服務端的一個加解密、編解碼過程。

須要注意的是,由於 AES 加解密時輸入和輸出都是二進制串的信息,所以,在發送時需先將明文經過 UTF-8 解碼成二進制串,而後進行加密,再對這串二進制密文經過 BASE64 編碼成密文串發送給接收方。

接收方的流程就是反着來一遍就對了。

代碼

理論上基本清楚了,那麼接下去就是代碼實現了,Android 項目中要實現很簡單,由於 JDK 和 SDK 中都已經將這些算法封裝好了,直接調用 api 接口就能夠了。

Java

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++

c++ 坑爹的地方就在於,這整個流程,包括 UTF-8 編解碼、AES 加解密、BASE64 編解碼都得本身寫。

固然,不可能本身寫了,網上輪子那麼多了,但問題就在於,由於 AES 加解密模式太多了,網上的資料大部分都只是針對其中一種進行介紹,所以,若是不稍微瞭解一下相關原理的話,就無從下手進行修改了。

我這篇,天然也只是介紹我所使用的模式,若是你恰好跟我同樣,那也許能夠幫到你,若是跟你不同,至少我列出了資料的來源,整篇下來也稍微講了一些基礎性的原理,掌握這些,作點兒修修補補應該是能夠的。

貼代碼前,先將我所使用的模式列出來:

UTF-8 + AES(16位密鑰 + CBC + PKCS5Padding) + BASE64

其實這些都相似於工具類,官方庫沒提供,那網上找個輪子就行了,都是一個 h 和 cpp 文件而已,複製粘貼下就能夠了。重點在於準備好了這些工具類後,怎麼用,怎麼稍微修改。

若是你不想本身網上找,那下面我已經將相關連接都貼出來了,去複製粘貼下就能夠了。

c++ string、UTF8相互轉換方法

C++使用AES+Base64算法對文本進行加密

我最開始就是拿的第二篇來用的,而後才發現他所採用的模式是: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),若是你以爲本篇內容有幫助到你,能夠轉載但記得要關注,要標明原文哦,謝謝支持~ dasuAndroidTv2.png

相關文章
相關標籤/搜索