在.NET Core中使用MachineKey
在上篇文章中,我介紹了Cookie
是基於MachineKey
生成的,MachineKey
決定了Cookie
生成的算法和密鑰,並如果使用多台服務器做負載均衡時,必須指定一致的MachineKey
。
但在.NET Core
中,官方似乎並沒有提供MachineKey
實現,這為兼容.NET Framework
的Cookie
造成了許多障礙。
今天我將深入探索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/但
MachineKey
的validationKey
和decryptionKey
的內容只要符合長度和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);
}
}
可見它本質是依賴於AspNetCryptoServiceProvider
,它使用了MachineKeyCryptoAlgorithmFactory
、MachineKeyMasterKeyProvider
、MachineKeyDataProtectorFactory
,以及一個看上去有點奇怪的SP800_108.DeriveKey
。
AspNetCryptoServiceProvider
的GetCryptoService
方法如下:
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
,這兩個就是之前所說的MachineKeyMasterKeyProvider
和MachineKeyDataProtectorFactory
。
注意其中還有一個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
、密文
以及簽名
三部分組成;
其中密文
使用encryptionKey
、IV
和原始明文
加密而來;
簽名
由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
}
可見非常地直白、淺顯易懂。
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
}
}
注意_machineKeySection
的ApplicationName
和DataProtectorType
默認都是空字符串""
,具體不細說,在這定義的: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騷操作】