//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ namespace System.Web.Security.Cryptography { using System; using System.Collections.Generic; using System.IO; using System.Text; // Represents a purpose that can be passed to a cryptographic routine to control key derivation / ciphertext modification. // This is hardening the crypto routines to prevent playing ciphertext off of components that didn't generate them. // // !! IMPORTANT !! // The built-in purposes do not contain privileged information and are not meant to be treated as secrets. Any external // person can disassemble our code or look directly at our source to see what our Purpose objects are used for. // // PrimaryPurpose: This is a well-known string that identifies the reason for this Purpose. The pattern we use // is that PrimaryPurpose is the name of the consumer, making each consumer's Purpose unique. // // SpecificPurposes: These are extra optional strings that further differentiate Purpose objects that might have the // same PrimaryPurpose. The pattern we use is that if a single consumer has multiple Purposes, he can use // SpecificPurposes to uniquely identify them. The information here is generally not secret (we can put the type of // the currently executing Page here, for example), but it is valid to seed this property with a secret obtained // at runtime (such as a nonce shared between two parties). internal sealed class Purpose { // predefined purposes public static readonly Purpose AnonymousIdentificationModule_Ticket = new Purpose("AnonymousIdentificationModule.Ticket"); public static readonly Purpose AssemblyResourceLoader_WebResourceUrl = new Purpose("AssemblyResourceLoader.WebResourceUrl"); public static readonly Purpose FormsAuthentication_Ticket = new Purpose("FormsAuthentication.Ticket"); public static readonly Purpose WebForms_Page_PreviousPageID = new Purpose("WebForms.Page.PreviousPageID"); public static readonly Purpose RolePrincipal_Ticket = new Purpose("RolePrincipal.Ticket"); public static readonly Purpose ScriptResourceHandler_ScriptResourceUrl = new Purpose("ScriptResourceHandler.ScriptResourceUrl"); // predefined ViewState purposes; they won't be used as-is (they're combined with the page information) public static readonly Purpose WebForms_ClientScriptManager_EventValidation = new Purpose("WebForms.ClientScriptManager.EventValidation"); public static readonly Purpose WebForms_DetailsView_KeyTable = new Purpose("WebForms.DetailsView.KeyTable"); public static readonly Purpose WebForms_GridView_DataKeys = new Purpose("WebForms.GridView.DataKeys"); public static readonly Purpose WebForms_GridView_SortExpression = new Purpose("WebForms.GridView.SortExpression"); public static readonly Purpose WebForms_HiddenFieldPageStatePersister_ClientState = new Purpose("WebForms.HiddenFieldPageStatePersister.ClientState"); public static readonly Purpose WebForms_ScriptManager_HistoryState = new Purpose("WebForms.ScriptManager.HistoryState"); public static readonly Purpose WebForms_SessionPageStatePersister_ClientState = new Purpose("WebForms.SessionPageStatePersister.ClientState"); // predefined miscellaneoous purposes; they won't be used as-is (they're combined with other specificPurposes) public static readonly Purpose User_MachineKey_Protect = new Purpose("User.MachineKey.Protect"); // used by the MachineKey static class Protect / Unprotect methods public static readonly Purpose User_ObjectStateFormatter_Serialize = new Purpose("User.ObjectStateFormatter.Serialize"); // used by ObjectStateFormatter.Serialize() if called manually public readonly string PrimaryPurpose; public readonly string[] SpecificPurposes; private byte[] _derivedKeyLabel; private byte[] _derivedKeyContext; public Purpose(string primaryPurpose, params string[] specificPurposes) : this(primaryPurpose, specificPurposes, null, null) { } // ctor for unit testing internal Purpose(string primaryPurpose, string[] specificPurposes, CryptographicKey derivedEncryptionKey, CryptographicKey derivedValidationKey) { PrimaryPurpose = primaryPurpose; SpecificPurposes = specificPurposes ?? new string[0]; DerivedEncryptionKey = derivedEncryptionKey; DerivedValidationKey = derivedValidationKey; SaveDerivedKeys = (SpecificPurposes.Length == 0); } // The cryptographic keys that were derived from this Purpose. internal CryptographicKey DerivedEncryptionKey { get; private set; } internal CryptographicKey DerivedValidationKey { get; private set; } // Whether the derived key should be saved back to this Purpose object by the ICryptoService, // e.g. because this Purpose will be used over and over again. We assume that any built-in // Purpose object that is passed without any specific purposes is intended for repeated use, // hence the ICryptoService will try to cache cryptographic keys as a performance optimization. // If specific purposes have been specified, they were likely generated at runtime, hence it // is not appropriate for the keys to be cached in this instance. internal bool SaveDerivedKeys { get; set; } // Returns a new Purpose which is the specified Purpose plus the specified SpecificPurpose. // Leaves the original Purpose unmodified. internal Purpose AppendSpecificPurpose(string specificPurpose) { // Append the specified specificPurpose to the existing list string[] newSpecificPurposes = new string[SpecificPurposes.Length + 1]; Array.Copy(SpecificPurposes, newSpecificPurposes, SpecificPurposes.Length); newSpecificPurposes[newSpecificPurposes.Length - 1] = specificPurpose; return new Purpose(PrimaryPurpose, newSpecificPurposes); } // Returns a new Purpose which is the specified Purpose plus the specified SpecificPurposes. // Leaves the original Purpose unmodified. internal Purpose AppendSpecificPurposes(IList specificPurposes) { // No specific purposes to add if (specificPurposes == null || specificPurposes.Count == 0) { return this; } // Append the specified specificPurposes to the existing list string[] newSpecificPurposes = new string[SpecificPurposes.Length + specificPurposes.Count]; Array.Copy(SpecificPurposes, newSpecificPurposes, SpecificPurposes.Length); specificPurposes.CopyTo(newSpecificPurposes, SpecificPurposes.Length); return new Purpose(PrimaryPurpose, newSpecificPurposes); } public CryptographicKey GetDerivedEncryptionKey(IMasterKeyProvider masterKeyProvider, KeyDerivationFunction keyDerivationFunction) { // has a key already been stored? CryptographicKey actualDerivedKey = DerivedEncryptionKey; if (actualDerivedKey == null) { CryptographicKey masterKey = masterKeyProvider.GetEncryptionKey(); actualDerivedKey = keyDerivationFunction(masterKey, this); // only save the key back to storage if this Purpose is configured to do so if (SaveDerivedKeys) { DerivedEncryptionKey = actualDerivedKey; } } return actualDerivedKey; } public CryptographicKey GetDerivedValidationKey(IMasterKeyProvider masterKeyProvider, KeyDerivationFunction keyDerivationFunction) { // has a key already been stored? CryptographicKey actualDerivedKey = DerivedValidationKey; if (actualDerivedKey == null) { CryptographicKey masterKey = masterKeyProvider.GetValidationKey(); actualDerivedKey = keyDerivationFunction(masterKey, this); // only save the key back to storage if this Purpose is configured to do so if (SaveDerivedKeys) { DerivedValidationKey = actualDerivedKey; } } return actualDerivedKey; } // 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; } } }