在.NET Core中使用MachineKey


在.NET Core中使用MachineKey

姐妹篇:《ASP.NET Cookie是怎么生成的》

姐妹篇:《.NET Core驗證ASP.NET密碼》

在上篇文章中,我介紹了Cookie是基於MachineKey生成的,MachineKey決定了Cookie生成的算法和密鑰,並如果使用多台服務器做負載均衡時,必須指定一致的MachineKey

但在.NET Core中,官方似乎並沒有提供MachineKey實現,這為兼容.NET FrameworkCookie造成了許多障礙。

今天我將深入探索MachineKey這個類,看看里面到底藏了什么東西,本文的最后我將使用.NET Core來解密一個ASP.NET MVC生成的Cookie

認識MachineKey

.NET Framework中,machineKey首先需要一個配置,寫在app.config或者web.config中,格式一般如下:

<machineKey validationKey="128個hex字符" decryptionKey="64個hex字符" validation="SHA1" decryption="AES" />

網上能找到可以直接生成隨機MachineKey的網站:https://www.developerfusion.com/tools/generatemachinekey/

MachineKeyvalidationKeydecryptionKey的內容只要符合長度和hex要求,都是可以隨意指定的,所以machineKey生成器的意義其實不大。

探索MachineKey

打開MachineKey的源代碼如下所示(有刪減):

public static class MachineKey {
    public static byte[] Unprotect(byte[] protectedData, params string[] purposes) {
        // ...有刪減

        return Unprotect(AspNetCryptoServiceProvider.Instance, protectedData, purposes);
    }

    // Internal method for unit testing.
    internal static byte[] Unprotect(ICryptoServiceProvider cryptoServiceProvider, byte[] protectedData, string[] purposes) {
        // If the user is calling this method, we want to use the ICryptoServiceProvider
        // regardless of whether or not it's the default provider.

        Purpose derivedPurpose = Purpose.User_MachineKey_Protect.AppendSpecificPurposes(purposes);
        ICryptoService cryptoService = cryptoServiceProvider.GetCryptoService(derivedPurpose);
        return cryptoService.Unprotect(protectedData);
    }
}

具體代碼可參見:https://referencesource.microsoft.com/#system.web/Security/MachineKey.cs,209

可見它本質是使用了AspNetCryptoServiceProvider.Instance,然后調用其GetCryptoService方法,然后獲取一個cryptoService,最后調用Unprotect,注意其中還使用了一個Purpose的類,依賴非常多。

AspNetCryptoServiceProvider

其中AspNetCryptoServiceProvider.Instance的定義如下(有刪減和整合):

internal sealed class AspNetCryptoServiceProvider : ICryptoServiceProvider {
    private static readonly Lazy<AspNetCryptoServiceProvider> _singleton = new Lazy<AspNetCryptoServiceProvider>(GetSingletonCryptoServiceProvider);

    internal static AspNetCryptoServiceProvider Instance {
        get {
            return _singleton.Value;
        }
    }

    private static AspNetCryptoServiceProvider GetSingletonCryptoServiceProvider() {
        // Provides all of the necessary dependencies for an application-level
        // AspNetCryptoServiceProvider.

        MachineKeySection machineKeySection = MachineKeySection.GetApplicationConfig();

        return new AspNetCryptoServiceProvider(
            machineKeySection: machineKeySection,
            cryptoAlgorithmFactory: new MachineKeyCryptoAlgorithmFactory(machineKeySection),
            masterKeyProvider: new MachineKeyMasterKeyProvider(machineKeySection),
            dataProtectorFactory: new MachineKeyDataProtectorFactory(machineKeySection),
            keyDerivationFunction: SP800_108.DeriveKey);
    }
}

具體代碼可見:https://referencesource.microsoft.com/#system.web/Security/Cryptography/AspNetCryptoServiceProvider.cs,68dbd1c184ea4e88

可見它本質是依賴於AspNetCryptoServiceProvider,它使用了MachineKeyCryptoAlgorithmFactoryMachineKeyMasterKeyProviderMachineKeyDataProtectorFactory,以及一個看上去有點奇怪的SP800_108.DeriveKey

AspNetCryptoServiceProviderGetCryptoService方法如下:

public ICryptoService GetCryptoService(Purpose purpose, CryptoServiceOptions options = CryptoServiceOptions.None) {
    ICryptoService cryptoService;
    if (_isDataProtectorEnabled && options == CryptoServiceOptions.None) {
        // We can only use DataProtector if it's configured and the caller didn't ask for any special behavior like cacheability
        cryptoService = GetDataProtectorCryptoService(purpose);
    }
    else {
        // Otherwise we fall back to using the <machineKey> algorithms for cryptography
        cryptoService = GetNetFXCryptoService(purpose, options);
    }

    // always homogenize errors returned from the crypto service
    return new HomogenizingCryptoServiceWrapper(cryptoService);
}

private NetFXCryptoService GetNetFXCryptoService(Purpose purpose, CryptoServiceOptions options) {
    // Extract the encryption and validation keys from the provided Purpose object
    CryptographicKey encryptionKey = purpose.GetDerivedEncryptionKey(_masterKeyProvider, _keyDerivationFunction);
    CryptographicKey validationKey = purpose.GetDerivedValidationKey(_masterKeyProvider, _keyDerivationFunction);

    // and return the ICryptoService
    // (predictable IV turned on if the caller requested cacheable output)
    return new NetFXCryptoService(_cryptoAlgorithmFactory, encryptionKey, validationKey, predictableIV: (options == CryptoServiceOptions.CacheableOutput));
}

注意其中有一個判斷,我結合dnSpy做了認真的調試,發現它默認走的是GetNetFXCryptoService,也就是注釋中所謂的<machineKey>算法。

然后GetNetFXCryptoService方法依賴於_masterKeyProvider_keyDerivationFunction用來生成兩個CryptographicKey,這兩個就是之前所說的MachineKeyMasterKeyProviderMachineKeyDataProtectorFactory

注意其中還有一個HomogenizingCryptoServiceWrapper類,故名思義,它的作用應該是統一管理加密解釋過程中的報錯,實際也確實如此,我不作深入,有興趣的讀者可以看看原始代碼在這:https://referencesource.microsoft.com/#system.web/Security/Cryptography/HomogenizingCryptoServiceWrapper.cs,25

最后調用NetFXCryptoService來執行Unprotect任務。

NetFXCryptoService

這個是重點了,源代碼如下(有刪減):

internal sealed class NetFXCryptoService : ICryptoService {

    private readonly ICryptoAlgorithmFactory _cryptoAlgorithmFactory;
    private readonly CryptographicKey _encryptionKey;
    private readonly bool _predictableIV;
    private readonly CryptographicKey _validationKey;

    // ...有刪減

    // [UNPROTECT]
    // INPUT: protectedData
    // OUTPUT: clearData
    // ALGORITHM:
    //   1) Assume protectedData := IV || Enc(Kenc, IV, clearData) || Sign(Kval, IV || Enc(Kenc, IV, clearData))
    //   2) Validate the signature over the payload and strip it from the end
    //   3) Strip off the IV from the beginning of the payload
    //   4) Decrypt what remains of the payload, and return it as clearData
    public byte[] Unprotect(byte[] protectedData) {
        // ...有刪減
        using (SymmetricAlgorithm decryptionAlgorithm = _cryptoAlgorithmFactory.GetEncryptionAlgorithm()) {
            // 省略約100行代碼😂
        }
    }
}

這個代碼非常長,我直接一刀全部刪減了,只保留注釋。如果不理解先好好看注釋,不理解它在干嘛,直接看代碼可能非常難,有興趣的可以直接先看看代碼:https://referencesource.microsoft.com/#system.web/Security/Cryptography/NetFXCryptoService.cs,35

首先看注釋:

protectedData := IV || Enc(Kenc, IV, clearData) || Sign(Kval, IV || Enc(Kenc, IV, clearData))

加密之后的數據由IV密文以及簽名三部分組成;

其中密文使用encryptionKeyIV原始明文加密而來;

簽名validationKey作驗證,傳入參數是IV以及密文(這一點有點像jwt)。

現在再來看看代碼:

int ivByteCount = decryptionAlgorithm.BlockSize / 8; // IV length is equal to the block size
int signatureByteCount = validationAlgorithm.HashSize / 8;

IV的長度由解密算法的BlockSize決定,簽名算法的長度由驗證算法的BlockSize決定,有了IV簽名的長度,就知道了密文的長度:

int encryptedPayloadByteCount = protectedData.Length - ivByteCount - signatureByteCount;

下文就應該是輕車熟路,依葫蘆畫瓢了,先驗證簽名:

byte[] computedSignature = validationAlgorithm.ComputeHash(protectedData, 0, ivByteCount + encryptedPayloadByteCount);
if (/*驗證不成功*/) {
    return null;
}

然后直接解密:

using (MemoryStream memStream = new MemoryStream()) {
    using (ICryptoTransform decryptor = decryptionAlgorithm.CreateDecryptor()) {
        using (CryptoStream cryptoStream = new CryptoStream(memStream, decryptor, CryptoStreamMode.Write)) {
            cryptoStream.Write(protectedData, ivByteCount, encryptedPayloadByteCount);
            cryptoStream.FlushFinalBlock();

            // At this point
            // memStream := clearData

            byte[] clearData = memStream.ToArray();
            return clearData;
        }
    }
}

可見這個類都是一些“正常操作”。之后我們來補充一下遺漏的部分。

MachineKeyCryptoAlgorithmFactory

首先是MachineKeyCryptoAlgorithmFactory,代碼如下(只保留了重點):

switch (algorithmName) {
    case "AES":
    case "Auto": // currently "Auto" defaults to AES
        return CryptoAlgorithms.CreateAes;

    case "DES":
        return CryptoAlgorithms.CreateDES;

    case "3DES":
        return CryptoAlgorithms.CreateTripleDES;

    default:
        return null; // unknown
}

switch (algorithmName) {
    case "SHA1":
        return CryptoAlgorithms.CreateHMACSHA1;

    case "HMACSHA256":
        return CryptoAlgorithms.CreateHMACSHA256;

    case "HMACSHA384":
        return CryptoAlgorithms.CreateHMACSHA384;

    case "HMACSHA512":
        return CryptoAlgorithms.CreateHMACSHA512;

    default:
        return null; // unknown
}

源代碼鏈接在這:https://referencesource.microsoft.com/#system.web/Security/Cryptography/MachineKeyCryptoAlgorithmFactory.cs,14

可見非常地直白、淺顯易懂。

MachineKeyMasterKeyProvider

然后是MachineKeyMasterKeyProvider,核心代碼如下:

private CryptographicKey GenerateCryptographicKey(string configAttributeName, string configAttributeValue, int autogenKeyOffset, int autogenKeyCount, string errorResourceString) {
    byte[] keyMaterial = CryptoUtil.HexToBinary(configAttributeValue);

    // If <machineKey> contained a valid key, just use it verbatim.
    if (keyMaterial != null && keyMaterial.Length > 0) {
        return new CryptographicKey(keyMaterial);
    }

    // 有刪減
}

 public CryptographicKey GetEncryptionKey() {
    if (_encryptionKey == null) {
        _encryptionKey = GenerateCryptographicKey(
            configAttributeName: "decryptionKey",
            configAttributeValue: _machineKeySection.DecryptionKey,
            autogenKeyOffset: AUTOGEN_ENCRYPTION_OFFSET,
            autogenKeyCount: AUTOGEN_ENCRYPTION_KEYLENGTH,
            errorResourceString: SR.Invalid_decryption_key);
    }
    return _encryptionKey;
}

public CryptographicKey GetValidationKey() {
    if (_validationKey == null) {
        _validationKey = GenerateCryptographicKey(
            configAttributeName: "validationKey",
            configAttributeValue: _machineKeySection.ValidationKey,
            autogenKeyOffset: AUTOGEN_VALIDATION_OFFSET,
            autogenKeyCount: AUTOGEN_VALIDATION_KEYLENGTH,
            errorResourceString: SR.Invalid_validation_key);
    }
    return _validationKey;
}

可見這個類就是從app.config/web.config中讀取兩個xml位置的值,並轉換為CryptographicKey,然后CryptographicKey的本質就是一個字節數組byte[]

注意,原版的GenerateCrytographicKey函數其實很長,但重點確實就是前面這三行代碼,后面的是一些騷操作,可以自動從一些配置的位置生成machineKey,這應該和machineKey節點缺失或者不寫有關,不在本文考慮的范疇以內。有興趣的讀者可以參見原始代碼:https://referencesource.microsoft.com/#system.web/Security/Cryptography/MachineKeyMasterKeyProvider.cs,87

MachineKeyDataProtectorFactory

其源代碼如下(有刪減):

internal sealed class MachineKeyDataProtectorFactory : IDataProtectorFactory {
    public DataProtector GetDataProtector(Purpose purpose) {
        if (_dataProtectorFactory == null) {
            _dataProtectorFactory = GetDataProtectorFactory();
        }
        return _dataProtectorFactory(purpose);
    }

    private Func<Purpose, DataProtector> GetDataProtectorFactory() {
        string applicationName = _machineKeySection.ApplicationName;
        string dataProtectorTypeName = _machineKeySection.DataProtectorType;

        Func<Purpose, DataProtector> factory = purpose => {
            // Since the custom implementation might depend on the impersonated
            // identity, we must instantiate it under app-level impersonation.
            using (new ApplicationImpersonationContext()) {
                return DataProtector.Create(dataProtectorTypeName, applicationName, purpose.PrimaryPurpose, purpose.SpecificPurposes);
            }
        };
        // 刪減驗證factory的部分代碼和try-catch
        return factory; // we know at this point the factory is good
    }
}

其原始代碼如下:https://referencesource.microsoft.com/#System.Web/Security/Cryptography/MachineKeyDataProtectorFactory.cs,cc110253450fcb16

注意_machineKeySectionApplicationNameDataProtectorType默認都是空字符串"",具體不細說,在這定義的:https://referencesource.microsoft.com/#System.Web/Configuration/MachineKeySection.cs,50

所以我們繼續看DataProtector的代碼:

public abstract class DataProtector
{
    public static DataProtector Create(string providerClass,
                                        string applicationName,
                                        string primaryPurpose,
                                        params string[] specificPurposes)
    {
        // Make sure providerClass is not null - Other parameters checked in constructor
        if (null == providerClass)
            throw new ArgumentNullException("providerClass");

        // Create a DataProtector based on this type using CryptoConfig
        return (DataProtector)CryptoConfig.CreateFromName(providerClass, applicationName, primaryPurpose, specificPurposes);
    }
}

注意它唯一的引用CryptoConfig,已經屬於.NET Core已經包含的范疇了,因此沒必要繼續深入追蹤。

Purpose

注意一開始時,我們說到的Purpose,相關定義如下:

public class Purpose {
    // ...有刪減
    public static readonly Purpose User_MachineKey_Protect = new Purpose("User.MachineKey.Protect");
	internal Purpose AppendSpecificPurposes(IList<string> specificPurposes)
	{
		if (specificPurposes == null || specificPurposes.Count == 0)
		{
			return this;
		}
		string[] array = new string[SpecificPurposes.Length + specificPurposes.Count];
		Array.Copy(SpecificPurposes, array, SpecificPurposes.Length);
		specificPurposes.CopyTo(array, SpecificPurposes.Length);
		return new Purpose(PrimaryPurpose, array);
	}

    // Returns a label and context suitable for passing into the SP800-108 KDF.
    internal void GetKeyDerivationParameters(out byte[] label, out byte[] context) {
        // The primary purpose can just be used as the label directly, since ASP.NET
        // is always in full control of the primary purpose (it's never user-specified).
        if (_derivedKeyLabel == null) {
            _derivedKeyLabel = CryptoUtil.SecureUTF8Encoding.GetBytes(PrimaryPurpose);
        }

        // The specific purposes (which can contain nonce, identity, etc.) are concatenated
        // together to form the context. The BinaryWriter class prepends each element with
        // a 7-bit encoded length to guarantee uniqueness.
        if (_derivedKeyContext == null) {
            using (MemoryStream stream = new MemoryStream())
            using (BinaryWriter writer = new BinaryWriter(stream, CryptoUtil.SecureUTF8Encoding)) {
                foreach (string specificPurpose in SpecificPurposes) {
                    writer.Write(specificPurpose);
                }
                _derivedKeyContext = stream.ToArray();
            }
        }

        label = _derivedKeyLabel;
        context = _derivedKeyContext;
    }
}

注意其PrimaryPurpose值為:"User.MachineKey.Protect"

另外還需要記住這個GetKeyDerivationParameters方法,它將在接下來的SP800_108類中使用,它將PrimaryPurpose經過utf8編碼生成label參數,然后用所有的SpecificPurposes通過二進制序列化,生成context參數。

原始代碼鏈接:https://referencesource.microsoft.com/#System.Web/Security/Cryptography/Purpose.cs,6fd5fbe04ec71877

SP800_108

已經接近尾聲了,我們知道一個字符串要轉換為密鑰,就必須經過一個安全的哈希算法。之前我們接觸得最多的,是Rfc2898DeriveBytes,但它是為了保存密碼而設計的。這里不需要這么復雜,因此…….NET另寫了一個。

這個類代碼非常長,但好在它所有內容都兼容.NET Core,因此可以直接復制粘貼。

它的目的是通過Purpose來生成密鑰。有興趣的讀者可以了解一下其算法:https://referencesource.microsoft.com/#System.Web/Security/Cryptography/SP800_108.cs,38

收尾

關系圖整理

我已經盡力將代碼重點划出來,但仍然很復雜。這么多類,我最后理了一個關系圖,用於了解其調用、依賴鏈:

MachineKey
    Purpose
    AspNetCryptoServiceProvider
        MachineKeySection
        MachineKeyCryptoAlgorithmFactory
            CryptoAlgorithms
        MachineKeyMasterKeyProvider
            CryptographicKey
        MachineKeyDataProtectorFactory
            DataProtector
                CryptoConfig
        SP800_108

祖傳代碼

整理了這么久,沒有點干貨怎么能行?基於以上的整理,我寫了一份“祖傳代碼”,可以直接拿來在.NET Core中使用。代碼較長,約200行,已經上傳到我的博客數據網站,各位可以自取:https://github.com/sdcb/blog-data/tree/master/2020/20200222-machinekey-in-dotnetcore

其實只要一行代碼?

直到后來,我發現有人將這些功能封閉成了一個NuGet包:AspNetTicketBridge,只需“一行”代碼,就能搞定所有這些功能:

// https://github.com/dmarlow/AspNetTicketBridge
string cookie = "你的Cookie內容";
string validationKey = "machineKey中的validationKey";
string decryptionKey = "machineKey中的decryptionKey";

OwinAuthenticationTicket ticket = MachineKeyTicketUnprotector.UnprotectCookie(cookie, decryptionKey, validationKey);

LINQPad運行,結果如下(完美破解):

總結

喜歡的朋友請關注我的微信公眾號:【DotNet騷操作】

DotNet騷操作


免責聲明!

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



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