正確使用AES對稱加密


正確使用AES對稱加密

經常我看到項目中有人使用了對稱加密算法,用來加密客戶或項目傳輸中的部分數據。但我注意到開發 人員由於不熟悉原理,或者簡單復制網上的代碼示例,有導致代碼存在安全風險。

我經常遇到的問題,有如下:

  • 如使用了過時的加密算法(如DES)
  • 設置了不安全的加密模式(ECB)
  • 不正確地處理初始向量(IV)

對稱加密算法

算法 位長 建議
RC4 40
DES 56
3DES 112
AES 128

TL;DR:

RC4/DES/3DES都 不符合 加密/破解的安全性要求。

DES是56位加密,聽起來感覺3DES應該是168位,但實際上其有效加密位長只有112位。

其它更長的加密算法,如AES 192位/AES 256位也符合要求。

加密模式

TL;DR: 不要使用ECB。

ECB不需要初始向量(IV),這個“驚人”的發現常常讓開發簡單粗暴地設計為ECB。ECB的問題在於輸入和輸出存在非常明顯的關聯,攻擊者可以從輸出輕松地猜出輸入數據。

C#的AES算法默認模式為CBC,該算法沒有上述的安全問題,而且最為通用,可以使用該模式。

初始向量

TL;DR:
初始向量 必須 為完全隨機數,完全隨機數應該使用RandomNumberGenerator進行加密。

回想這個問題,數據加密完后,該發送什么給接收方?僅數據?那么初始向量(IV)怎么辦?

大多數開發選擇的辦法是,寫一個固定的初始向量(IV)用於加密,然后解密時,也使用相同的初始向量。這樣就導致相同的輸入會產生相同的輸出

為什么相同的輸入應該產生不同的輸出?因為根據歷史經驗,攻擊者可以獲取一些信息,知道某個確定輸入的含義。一旦再次捕獲到相同的加密數據,就能輕易破解。

所以,發送數據應該包含:版本+初始向量+數據。

面向字符串

加密是面向字節還是字符串?我認為應該面向字節。如果面向字符串,那么很多問題很難受到重視。

試着回答這個問題:

  • 用戶的密碼是什么樣子的?
  • 是長度為固定32位的HEX字符嗎?如1C8F7B2C9759209C6ACC3C105D39BBAC
  • 還是用戶想輸入什么就輸入什么?如My-Super-Str0ng-Password!!

我認為加密算法應該面向字節流/字節數據,而不是字符串。將字符串發送給客戶、放在JSON中進行端對端傳輸,是沒什么毛病的做法。但基於以下原因,我強烈建議加密/解密算法要基於字節數據:

  • 避免密碼太長或太短的問題
  • 來回轉換為字符串效率低下
  • 字符串轉換為字節數組容易,其它數據序列化為字節數據也容易

我的加密/解密方法

// 代碼按原樣提供,可隨意使用,但不對其安全性作任何保證。
string Encrypt(string password, string purpose, byte[] plainBytes)
{
	byte[] key = PasswordToKey(password, purpose);
	using (var aes = Aes.Create())
	{
		aes.Key = key;
		using (ICryptoTransform encryptor = aes.CreateEncryptor())
		{
			byte[] cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
			byte[] packedBytes = Pack(
				version: 1, 
				iv: aes.IV, 
				cipherBytes: cipherBytes);
			return Base64UrlEncode(packedBytes);
		}
	}
}

byte[] Decrypt(string packedString, string password, string purpose)
{
	byte[] key = PasswordToKey(password, purpose);
	byte[] packedBytes = Base64UrlDecode(packedString);
	(byte version, byte[] iv, byte[] cipherBytes) = Unpack(packedBytes);
	using (var aes = Aes.Create())
	{
		using (ICryptoTransform decryptor = aes.CreateDecryptor(key, iv))
		{
			return decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
		}
	}
}

其中公共方法:

// 代碼按原樣提供,可隨意使用,但不對其安全性作任何保證。
byte[] PasswordToKey(string password, string purpose)
{
	using (var hmac = new HMACMD5(Encoding.UTF8.GetBytes(purpose)))
	{
		return hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
	}
}

string Base64UrlEncode(byte[] bytes)
{
	return Convert.ToBase64String(bytes)
			.Replace("/", "_")
			.Replace("+", "-")
			.Replace("=", "");
}

byte[] Base64UrlDecode(string base64Url)
{
	return Convert.FromBase64String(base64Url
		.Replace("_", "/")
		.Replace("-", "+"));
}

(byte version, byte[] iv, byte[] cipherBytes) Unpack(byte[] packedBytes)
{
	if (packedBytes[0] == 1)
	{
		// version 1
		return (1, packedBytes[1..1 + 16], packedBytes[1 + 16..]);
	}
	else
	{
		throw new NotImplementedException("unknown version");
	}
}

byte[] Pack(byte version, byte[] iv, byte[] cipherBytes)
{
	return new[] { version }.Concat(iv).Concat(cipherBytes).ToArray();
}

解釋:

  • Base64UrlEncode/Decode:用於將字符串在Url上傳輸,將+/=轉換成:-_
  • Pack/Unpack:將版本/初始向量/密文打包/解包
  • PasswordToKey:將長度不一樣密碼,加上purpose,轉換為長度一樣的key,其中改成HMACSHA256可以使用256位的AES算法。

測試代碼:

// 代碼按原樣提供,可隨意使用,但不對其安全性作任何保證。
string purpose = "這個算法是用來搞SSO的";
// 返回:AcfCe3AQcmNkeNThv-u09H_HyGKy_iRy-7uGiW0IZOHI
Encrypt("密碼here", purpose, Encoding.UTF8.GetBytes("Hello World"));
// 返回:Hello World
Encoding.UTF8.GetString(Decrypt("AcfCe3AQcmNkeNThv-u09H_HyGKy_iRy-7uGiW0IZOHI", "密碼here", purpose));


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM