探討NET Core資料進行3DES加密或解密弱金鑰問題

Jeffcky發表於2020-07-04

前言

之前寫過一篇《探討.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弱金鑰問題,同時和第三方對接時實在懶得去理解各語言實現加密演算法原理,可嘗試採用上述包來進行互操作,看到有幾位童鞋在文章下提出這個問題而苦於沒找到解決方案,這裡提供一種可選擇的方案,都已封裝好,拿去用吧。

相關文章