前言
之前寫過一篇《探討.NET Core資料進行3DES加密和解密問題》,最近看到有人提出弱金鑰問題,換個強金鑰不就完了嗎,猜測可能是與第三方對接導致很無奈不能更換金鑰,所以產生本文解決.NET Core中3DES弱金鑰問題,寫下本文,希望對碰到此問題的童鞋有所幫助。
3DES加密或解密弱金鑰
在基於.NET Framework中,我們可以使用反射獲取到TripleDESCryptoServiceProvider的“_NewEncryptor”私有方法,從而規避判斷弱祕鑰問題,但在.NET Core中沒有這個方法,我們首先來看看問題的產生,如下為.NET Core中加密和解密的方法實現
public static string DesEncrypt(string input, string key) { byte[] inputArray = Encoding.UTF8.GetBytes(input); var tripleDES = TripleDES.Create(); var byteKey = Encoding.UTF8.GetBytes(key); byte[] allKey = new byte[24]; Buffer.BlockCopy(byteKey, 0, allKey, 0, 16); Buffer.BlockCopy(byteKey, 0, allKey, 16, 8); tripleDES.Key = allKey; tripleDES.Mode = CipherMode.ECB; tripleDES.Padding = PaddingMode.PKCS7; ICryptoTransform cTransform = tripleDES.CreateEncryptor(); byte[] resultArray = cTransform.TransformFinalBlock(inputArray, 0, inputArray.Length); return Convert.ToBase64String(resultArray, 0, resultArray.Length); } public static string DesDecrypt(string input, string key) { byte[] inputArray = Convert.FromBase64String(input); var tripleDES = TripleDES.Create(); var byteKey = Encoding.UTF8.GetBytes(key); byte[] allKey = new byte[24]; Buffer.BlockCopy(byteKey, 0, allKey, 0, 16); Buffer.BlockCopy(byteKey, 0, allKey, 16, 8); tripleDES.Key = byteKey; tripleDES.Mode = CipherMode.ECB; tripleDES.Padding = PaddingMode.PKCS7; ICryptoTransform cTransform = tripleDES.CreateDecryptor(); byte[] resultArray = cTransform.TransformFinalBlock(inputArray, 0, inputArray.Length); return Encoding.UTF8.GetString(resultArray); }
接下來我們呼叫上述加密方法對資料進行加密,當然這裡的金鑰很簡單為16位1,NET Framework中對弱金鑰的具體判斷邏輯這裡不做深入分析,如下:
var desEncryptData = DesEncrypt("Jeffcky", "1111111111111111");
為解決這個問題我們下載BouncyCastle.NetCore包(https://github.com/chrishaly/bc-csharp),此包有針對基本所有加密演算法實現,你會發現通過該包實現和Java中加密演算法實現非常相似,若與第三方Java對接,對方所傳資料可能利用.NET Core無法解密或通過加密導致對方無法解密,因為無論是C#還是Java對於演算法的實現還是有所差異,利用此包可以進行互操作。
在C#中3DES名稱定義為TripleDES,而在Java中名稱則是DESede,同時C#中的填充模式PKCS7對應Java中的PKCS5Padding,接下來你將看到如下C#程式碼幾乎就是從Java中翻譯過來,如下:
static IBufferedCipher CreateCipher(bool forEncryption, string key, string cipMode = "DESede/ECB/PKCS5Padding") { var algorithmName = cipMode; if (cipMode.IndexOf('/') >= 0) { algorithmName = cipMode.Substring(0, cipMode.IndexOf('/')); } var cipher = CipherUtilities.GetCipher(cipMode); var keyBytes = Encoding.UTF8.GetBytes(key); var keyParameter = ParameterUtilities.CreateKeyParameter(algorithmName, keyBytes); cipher.Init(forEncryption, keyParameter); return cipher; }
如上主要是建立加密演算法介面(預設為3DES),若forEncryption為true表示加密,否則解密,具體細節這裡就不再詳細解釋,有興趣的童鞋可自行研究。接下來我們實現加密和解密方法:
static string EncryptData(string input, string key) { var inCipher = CreateCipher(true, key); var inputArray = Encoding.UTF8.GetBytes(input); byte[] cipherData = inCipher.DoFinal(inputArray); return Convert.ToBase64String(cipherData); } static string DecryptData(string input, string key) { var inputArrary = Convert.FromBase64String(input); var outCipher = CreateCipher(false, key); var encryptedDataStream = new MemoryStream(inputArrary, false); var dataStream = new MemoryStream(); var outCipherStream = new CipherStream(dataStream, null, outCipher); int ch; while ((ch = encryptedDataStream.ReadByte()) >= 0) { outCipherStream.WriteByte((byte)ch); } outCipherStream.Close(); encryptedDataStream.Close(); var dataBytes = dataStream.ToArray(); return Encoding.UTF8.GetString(dataBytes); }
雖然金鑰是16位,但在內建具體實現時也會如.NET Core中一樣填充到24位,接下來我們再來呼叫上述加密和解密方法,看看資料加密和解密是否正確
var data = EncryptData("Jeffcky", "1111111111111111"); var decryptData = DecryptData(data, "1111111111111111");
那麼問題來了,為何在C#中會丟擲弱金鑰異常,但是在這個包中卻沒能丟擲異常呢?內建是基於Schneier pp281的弱和半弱鍵表進行查詢可能與C#實現邏輯有所不同(個人猜測),如下:
public const int DesKeyLength = 8; private const int N_DES_WEAK_KEYS = 16; //基於Schneier pp281的弱和半弱鍵表 private static readonly byte[] DES_weak_keys = { /* 弱鍵 */ (byte)0x01,(byte)0x01,(byte)0x01,(byte)0x01, (byte)0x01,(byte)0x01,(byte)0x01,(byte)0x01, (byte)0x1f,(byte)0x1f,(byte)0x1f,(byte)0x1f, (byte)0x0e,(byte)0x0e,(byte)0x0e,(byte)0x0e, (byte)0xe0,(byte)0xe0,(byte)0xe0,(byte)0xe0, (byte)0xf1,(byte)0xf1,(byte)0xf1,(byte)0xf1, (byte)0xfe,(byte)0xfe,(byte)0xfe,(byte)0xfe, (byte)0xfe,(byte)0xfe,(byte)0xfe,(byte)0xfe, /* 半弱鍵 */ (byte)0x01,(byte)0xfe,(byte)0x01,(byte)0xfe, (byte)0x01,(byte)0xfe,(byte)0x01,(byte)0xfe, (byte)0x1f,(byte)0xe0,(byte)0x1f,(byte)0xe0, (byte)0x0e,(byte)0xf1,(byte)0x0e,(byte)0xf1, (byte)0x01,(byte)0xe0,(byte)0x01,(byte)0xe0, (byte)0x01,(byte)0xf1,(byte)0x01,(byte)0xf1, (byte)0x1f,(byte)0xfe,(byte)0x1f,(byte)0xfe, (byte)0x0e,(byte)0xfe,(byte)0x0e,(byte)0xfe, (byte)0x01,(byte)0x1f,(byte)0x01,(byte)0x1f, (byte)0x01,(byte)0x0e,(byte)0x01,(byte)0x0e, (byte)0xe0,(byte)0xfe,(byte)0xe0,(byte)0xfe, (byte)0xf1,(byte)0xfe,(byte)0xf1,(byte)0xfe, (byte)0xfe,(byte)0x01,(byte)0xfe,(byte)0x01, (byte)0xfe,(byte)0x01,(byte)0xfe,(byte)0x01, (byte)0xe0,(byte)0x1f,(byte)0xe0,(byte)0x1f, (byte)0xf1,(byte)0x0e,(byte)0xf1,(byte)0x0e, (byte)0xe0,(byte)0x01,(byte)0xe0,(byte)0x01, (byte)0xf1,(byte)0x01,(byte)0xf1,(byte)0x01, (byte)0xfe,(byte)0x1f,(byte)0xfe,(byte)0x1f, (byte)0xfe,(byte)0x0e,(byte)0xfe,(byte)0x0e, (byte)0x1f,(byte)0x01,(byte)0x1f,(byte)0x01, (byte)0x0e,(byte)0x01,(byte)0x0e,(byte)0x01, (byte)0xfe,(byte)0xe0,(byte)0xfe,(byte)0xe0, (byte)0xfe,(byte)0xf1,(byte)0xfe,(byte)0xf1 }; public static bool IsWeakKey(byte[] key, int offset) { if (key.Length - offset < DesKeyLength) throw new ArgumentException("key material too short."); //nextkey: for (int i = 0; i < N_DES_WEAK_KEYS; i++) { bool unmatch = false; for (int j = 0; j < DesKeyLength; j++) { if (key[j + offset] != DES_weak_keys[i * DesKeyLength + j]) { //continue nextkey; unmatch = true; break; } } if (!unmatch) { return true; } } return false; }
如果第三方為Java,當利用.NET Core實在走投無路無法進行解密時,那就使用上述提供的解密方法進行解密,理論上都可以解密,不能解密的情況大多出現於對C#和Java實現原理不瞭解導致,如下:
總結
本文重點在於解決.NET Core中3DES弱金鑰問題,同時和第三方對接時實在懶得去理解各語言實現加密演算法原理,可嘗試採用上述包來進行互操作,看到有幾位童鞋在文章下提出這個問題而苦於沒找到解決方案,這裡提供一種可選擇的方案,都已封裝好,拿去用吧。