1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
|
//------------------------------------------------------------------------------
// <copyright file="EventValidationStore.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
namespace System.Web.UI {
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Web.Security.Cryptography;
using System.Web.Util;
// Represents a store of all of the event validation (target, argument) tuples
// that are valid for a given WebForms page.
internal sealed class EventValidationStore {
// We don't want to use a full SHA-256 hash since it produces an unacceptable increase in the size
// of the __EVENTVALIDATION field. Instead, we truncate the SHA-256 hash to 128 bits. This is
// acceptable according to the Crypto SDL v5.2.
private const int HASH_SIZE_IN_BYTES = 128 / 8;
// contains all cryptographic hashes which are known to this event validation instance
private readonly HashSet<byte[]> _hashes = new HashSet<byte[]>(HashEqualityComparer.Instance);
public int Count {
get {
return _hashes.Count;
}
}
public void Add(string target, string argument) {
_hashes.Add(Hash(target, argument));
}
// Creates a duplicate store seeded with the same hashes as the current store.
public EventValidationStore Clone() {
EventValidationStore newStore = new EventValidationStore();
newStore._hashes.UnionWith(this._hashes);
return newStore;
}
public bool Contains(string target, string argument) {
return _hashes.Contains(Hash(target, argument));
}
// Stores a string in a buffer at the specified offset. The string is stored as the
// 32-bit character count (big-endian) followed by the string data as UTF-16BE.
// Null strings are treated as equal to empty string. When the method completes, the
// 'offset' parameter will be updated to point *after* the string in the buffer.
private static void CopyStringToBuffer(string s, byte[] buffer, ref int offset) {
int stringLength = (s != null) ? s.Length : 0;
buffer[offset++] = (byte)(stringLength >> 24);
buffer[offset++] = (byte)(stringLength >> 16);
buffer[offset++] = (byte)(stringLength >> 8);
buffer[offset++] = (byte)(stringLength);
if (s != null) {
for (int i = 0; i < s.Length; i++) {
char c = s[i];
buffer[offset++] = (byte)(c >> 8);
buffer[offset++] = (byte)(c);
}
}
}
public static EventValidationStore DeserializeFrom(Stream inputStream) {
// don't need a 'using' block around this reader
DeserializingBinaryReader reader = new DeserializingBinaryReader(inputStream);
byte versionHeader = reader.ReadByte();
if (versionHeader != (byte)0x00) {
// the only version we support is v0; throw if unsupported
throw new InvalidOperationException(SR.GetString(SR.InvalidSerializedData));
}
EventValidationStore store = new EventValidationStore();
// 'numEntries' is the number of HASH_SIZE_IN_BYTES-sized entries
// we should expect in the stream.
int numEntries = reader.Read7BitEncodedInt();
for (int i = 0; i < numEntries; i++) {
byte[] entry = reader.ReadBytes(HASH_SIZE_IN_BYTES);
if (entry.Length != HASH_SIZE_IN_BYTES) {
// bad data (EOF)
throw new InvalidOperationException(SR.GetString(SR.InvalidSerializedData));
}
store._hashes.Add(entry);
}
return store;
}
private static byte[] Hash(string target, string argument) {
// This algorithm previously used MemoryStream and BinaryWriter, but this was causing a measurable
// performance hit since Event Validation code might be run in a tight loop. We'll instead just
// build up the buffer to be hashed manually.
int targetStringLength = (target != null) ? target.Length : 0; // null and empty 'target' treated equally
int argumentStringLength = (argument != null) ? argument.Length : 0; // null and empty 'argument' treated equally
byte[] bufferToBeHashed = new byte[8 + (targetStringLength + argumentStringLength) * 2]; // for each string, 4 bytes length prefix + (2 * length) bytes for UTF-16 payload
// copy strings into buffer
int currentOffset = 0;
CopyStringToBuffer(target, bufferToBeHashed, ref currentOffset);
CopyStringToBuffer(argument, bufferToBeHashed, ref currentOffset);
Debug.Assert(currentOffset == bufferToBeHashed.Length, "Should have populated the entire buffer.");
// hash the buffer
byte[] fullHash;
using (SHA256 hashAlgorithm = CryptoAlgorithms.CreateSHA256()) {
fullHash = hashAlgorithm.ComputeHash(bufferToBeHashed);
}
// truncate to desired size; SHA evenly distributes entropy throughout the generated hash,
// so for simplicity we'll just chop off the last several bytes
byte[] truncatedHash = new byte[HASH_SIZE_IN_BYTES];
Buffer.BlockCopy(fullHash, 0, truncatedHash, 0, HASH_SIZE_IN_BYTES);
return truncatedHash;
}
public void SerializeTo(Stream outputStream) {
// don't need a 'using' block around this writer
SerializingBinaryWriter writer = new SerializingBinaryWriter(outputStream);
writer.Write((byte)0x00); // version header
writer.Write7BitEncodedInt(_hashes.Count); // number of entries
foreach (byte[] entry in _hashes) {
writer.Write(entry);
}
}
private sealed class HashEqualityComparer : IEqualityComparer<byte[]> {
internal static readonly HashEqualityComparer Instance = new HashEqualityComparer();
private HashEqualityComparer() { }
public bool Equals(byte[] x, byte[] y) {
// The lengths of 'x' and 'y' are checked before the values are added to the HashSet.
// Add a debug assert here just to check it if we ever change the algorithm from SHA256.
Debug.Assert(x.Length == HASH_SIZE_IN_BYTES);
Debug.Assert(y.Length == HASH_SIZE_IN_BYTES);
// We're not too concerned about timing attacks here since the event validation
// hashes are all public knowledge.
for (int i = 0; i < HASH_SIZE_IN_BYTES; i++) {
if (x[i] != y[i]) { return false; }
}
return true;
}
public int GetHashCode(byte[] obj) {
// Since the incoming byte[] represents a cryptographic hash code, entropy should be
// approximately uniformly distributed throughout the entire array, so we can just
// treat the high 32 bits as the hash code for simplicity.
return BitConverter.ToInt32(obj, 0);
}
}
private sealed class DeserializingBinaryReader : BinaryReader {
public DeserializingBinaryReader(Stream input) : base(input) { }
protected override void Dispose(bool disposing) {
// Don't call base.Dispose(), since it disposes of the underlying stream,
// a behavior we don't want.
}
public new int Read7BitEncodedInt() {
return base.Read7BitEncodedInt();
}
}
private sealed class SerializingBinaryWriter : BinaryWriter {
public SerializingBinaryWriter(Stream input) : base(input) { }
protected override void Dispose(bool disposing) {
// Don't call base.Dispose(), since it disposes of the underlying stream,
// a behavior we don't want.
}
public new void Write7BitEncodedInt(int value) {
base.Write7BitEncodedInt(value);
}
}
}
}
|