正確使用AES對稱加密

正確使用AES對稱加密

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

我常常遇到的問題,有以下:c#

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

對稱加密算法

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

TL;DR:數組

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

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

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

加密模式

TL;DR: 不要使用ECB。加密

ECB不須要初始向量(IV),這個「驚人」的發現經常讓開發簡單粗暴地設計爲ECB。ECB的問題在於輸入和輸出存在很是明顯的關聯,攻擊者能夠從輸出輕鬆地猜出輸入數據。
ECB加密設計

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

初始向量

TL;DR:
初始向量 必須 爲徹底隨機數,徹底隨機數應該使用RandomNumberGenerator進行加密。orm

回想這個問題,數據加密完後,該發送什麼給接收方?僅數據?那麼初始向量(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));
相關文章
相關標籤/搜索