-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
Copy pathEventValidationStore.cs
190 lines (154 loc) · 8.45 KB
/
EventValidationStore.cs
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);
}
}
}
}