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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
|
//-----------------------------------------------------------------------
// <copyright file="DataBagFormatterBase.cs" company="Andrew Arnott">
// Copyright (c) Andrew Arnott. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.Messaging {
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.Messaging.Bindings;
using DotNetOpenAuth.Messaging.Reflection;
/// <summary>
/// A serializer for <see cref="DataBag"/>-derived types
/// </summary>
/// <typeparam name="T">The DataBag-derived type that is to be serialized/deserialized.</typeparam>
internal abstract class DataBagFormatterBase<T> : IDataBagFormatter<T> where T : DataBag, new() {
/// <summary>
/// The message description cache to use for data bag types.
/// </summary>
protected static readonly MessageDescriptionCollection MessageDescriptions = new MessageDescriptionCollection();
/// <summary>
/// The length of the nonce to include in tokens that can be decoded once only.
/// </summary>
private const int NonceLength = 6;
/// <summary>
/// The minimum allowable lifetime for the key used to encrypt/decrypt or sign this databag.
/// </summary>
private readonly TimeSpan minimumAge = TimeSpan.FromDays(1);
/// <summary>
/// The symmetric key store with the secret used for signing/encryption of verification codes and refresh tokens.
/// </summary>
private readonly ICryptoKeyStore cryptoKeyStore;
/// <summary>
/// The bucket for symmetric keys.
/// </summary>
private readonly string cryptoKeyBucket;
/// <summary>
/// The crypto to use for signing access tokens.
/// </summary>
private readonly RSACryptoServiceProvider asymmetricSigning;
/// <summary>
/// The crypto to use for encrypting access tokens.
/// </summary>
private readonly RSACryptoServiceProvider asymmetricEncrypting;
/// <summary>
/// A value indicating whether the data in this instance will be protected against tampering.
/// </summary>
private readonly bool signed;
/// <summary>
/// The nonce store to use to ensure that this instance is only decoded once.
/// </summary>
private readonly INonceStore decodeOnceOnly;
/// <summary>
/// The maximum age of a token that can be decoded; useful only when <see cref="decodeOnceOnly"/> is <c>true</c>.
/// </summary>
private readonly TimeSpan? maximumAge;
/// <summary>
/// A value indicating whether the data in this instance will be protected against eavesdropping.
/// </summary>
private readonly bool encrypted;
/// <summary>
/// A value indicating whether the data in this instance will be GZip'd.
/// </summary>
private readonly bool compressed;
/// <summary>
/// Initializes a new instance of the <see cref="DataBagFormatterBase<T>"/> class.
/// </summary>
/// <param name="signingKey">The crypto service provider with the asymmetric key to use for signing or verifying the token.</param>
/// <param name="encryptingKey">The crypto service provider with the asymmetric key to use for encrypting or decrypting the token.</param>
/// <param name="compressed">A value indicating whether the data in this instance will be GZip'd.</param>
/// <param name="maximumAge">The maximum age of a token that can be decoded; useful only when <paramref name="decodeOnceOnly"/> is <c>true</c>.</param>
/// <param name="decodeOnceOnly">The nonce store to use to ensure that this instance is only decoded once.</param>
protected DataBagFormatterBase(RSACryptoServiceProvider signingKey = null, RSACryptoServiceProvider encryptingKey = null, bool compressed = false, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null)
: this(signingKey != null, encryptingKey != null, compressed, maximumAge, decodeOnceOnly) {
this.asymmetricSigning = signingKey;
this.asymmetricEncrypting = encryptingKey;
}
/// <summary>
/// Initializes a new instance of the <see cref="DataBagFormatterBase<T>"/> class.
/// </summary>
/// <param name="cryptoKeyStore">The crypto key store used when signing or encrypting.</param>
/// <param name="bucket">The bucket in which symmetric keys are stored for signing/encrypting data.</param>
/// <param name="signed">A value indicating whether the data in this instance will be protected against tampering.</param>
/// <param name="encrypted">A value indicating whether the data in this instance will be protected against eavesdropping.</param>
/// <param name="compressed">A value indicating whether the data in this instance will be GZip'd.</param>
/// <param name="minimumAge">The required minimum lifespan within which this token must be decodable and verifiable; useful only when <paramref name="signed"/> and/or <paramref name="encrypted"/> is true.</param>
/// <param name="maximumAge">The maximum age of a token that can be decoded; useful only when <paramref name="decodeOnceOnly"/> is <c>true</c>.</param>
/// <param name="decodeOnceOnly">The nonce store to use to ensure that this instance is only decoded once.</param>
protected DataBagFormatterBase(ICryptoKeyStore cryptoKeyStore = null, string bucket = null, bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? minimumAge = null, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null)
: this(signed, encrypted, compressed, maximumAge, decodeOnceOnly) {
Requires.True(!String.IsNullOrEmpty(bucket) || cryptoKeyStore == null, null);
Requires.True(cryptoKeyStore != null || (!signed && !encrypted), null);
this.cryptoKeyStore = cryptoKeyStore;
this.cryptoKeyBucket = bucket;
if (minimumAge.HasValue) {
this.minimumAge = minimumAge.Value;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="DataBagFormatterBase<T>"/> class.
/// </summary>
/// <param name="signed">A value indicating whether the data in this instance will be protected against tampering.</param>
/// <param name="encrypted">A value indicating whether the data in this instance will be protected against eavesdropping.</param>
/// <param name="compressed">A value indicating whether the data in this instance will be GZip'd.</param>
/// <param name="maximumAge">The maximum age of a token that can be decoded; useful only when <paramref name="decodeOnceOnly"/> is <c>true</c>.</param>
/// <param name="decodeOnceOnly">The nonce store to use to ensure that this instance is only decoded once.</param>
private DataBagFormatterBase(bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null) {
Requires.True(signed || decodeOnceOnly == null, null);
Requires.True(maximumAge.HasValue || decodeOnceOnly == null, null);
this.signed = signed;
this.maximumAge = maximumAge;
this.decodeOnceOnly = decodeOnceOnly;
this.encrypted = encrypted;
this.compressed = compressed;
}
/// <summary>
/// Serializes the specified message, including compression, encryption, signing, and nonce handling where applicable.
/// </summary>
/// <param name="message">The message to serialize. Must not be null.</param>
/// <returns>A non-null, non-empty value.</returns>
public string Serialize(T message) {
message.UtcCreationDate = DateTime.UtcNow;
if (this.decodeOnceOnly != null) {
message.Nonce = MessagingUtilities.GetNonCryptoRandomData(NonceLength);
}
byte[] encoded = this.SerializeCore(message);
if (this.compressed) {
encoded = MessagingUtilities.Compress(encoded);
}
string symmetricSecretHandle = null;
if (this.encrypted) {
encoded = this.Encrypt(encoded, out symmetricSecretHandle);
}
if (this.signed) {
message.Signature = this.CalculateSignature(encoded, symmetricSecretHandle);
}
int capacity = this.signed ? 4 + message.Signature.Length + 4 + encoded.Length : encoded.Length;
using (var finalStream = new MemoryStream(capacity)) {
var writer = new BinaryWriter(finalStream);
if (this.signed) {
writer.WriteBuffer(message.Signature);
}
writer.WriteBuffer(encoded);
writer.Flush();
string payload = MessagingUtilities.ConvertToBase64WebSafeString(finalStream.ToArray());
string result = payload;
if (symmetricSecretHandle != null && (this.signed || this.encrypted)) {
result = MessagingUtilities.CombineKeyHandleAndPayload(symmetricSecretHandle, payload);
}
return result;
}
}
/// <summary>
/// Deserializes a <see cref="DataBag"/>, including decompression, decryption, signature and nonce validation where applicable.
/// </summary>
/// <param name="containingMessage">The message that contains the <see cref="DataBag"/> serialized value. Must not be nulll.</param>
/// <param name="value">The serialized form of the <see cref="DataBag"/> to deserialize. Must not be null or empty.</param>
/// <returns>The deserialized value. Never null.</returns>
public T Deserialize(IProtocolMessage containingMessage, string value) {
string symmetricSecretHandle = null;
if (this.encrypted && this.cryptoKeyStore != null) {
string valueWithoutHandle;
MessagingUtilities.ExtractKeyHandleAndPayload(containingMessage, "<TODO>", value, out symmetricSecretHandle, out valueWithoutHandle);
value = valueWithoutHandle;
}
var message = new T { ContainingMessage = containingMessage };
byte[] data = MessagingUtilities.FromBase64WebSafeString(value);
byte[] signature = null;
if (this.signed) {
using (var dataStream = new MemoryStream(data)) {
var dataReader = new BinaryReader(dataStream);
signature = dataReader.ReadBuffer();
data = dataReader.ReadBuffer();
}
// Verify that the verification code was issued by message authorization server.
ErrorUtilities.VerifyProtocol(this.IsSignatureValid(data, signature, symmetricSecretHandle), MessagingStrings.SignatureInvalid);
}
if (this.encrypted) {
data = this.Decrypt(data, symmetricSecretHandle);
}
if (this.compressed) {
data = MessagingUtilities.Decompress(data);
}
this.DeserializeCore(message, data);
message.Signature = signature; // TODO: we don't really need this any more, do we?
if (this.maximumAge.HasValue) {
// Has message verification code expired?
DateTime expirationDate = message.UtcCreationDate + this.maximumAge.Value;
if (expirationDate < DateTime.UtcNow) {
throw new ExpiredMessageException(expirationDate, containingMessage);
}
}
// Has message verification code already been used to obtain an access/refresh token?
if (this.decodeOnceOnly != null) {
ErrorUtilities.VerifyInternal(this.maximumAge.HasValue, "Oops! How can we validate a nonce without a maximum message age?");
string context = "{" + GetType().FullName + "}";
if (!this.decodeOnceOnly.StoreNonce(context, Convert.ToBase64String(message.Nonce), message.UtcCreationDate)) {
Logger.OpenId.ErrorFormat("Replayed nonce detected ({0} {1}). Rejecting message.", message.Nonce, message.UtcCreationDate);
throw new ReplayedMessageException(containingMessage);
}
}
((IMessage)message).EnsureValidMessage();
return message;
}
/// <summary>
/// Serializes the <see cref="DataBag"/> instance to a buffer.
/// </summary>
/// <param name="message">The message.</param>
/// <returns>The buffer containing the serialized data.</returns>
protected abstract byte[] SerializeCore(T message);
/// <summary>
/// Deserializes the <see cref="DataBag"/> instance from a buffer.
/// </summary>
/// <param name="message">The message instance to initialize with data from the buffer.</param>
/// <param name="data">The data buffer.</param>
protected abstract void DeserializeCore(T message, byte[] data);
/// <summary>
/// Determines whether the signature on this instance is valid.
/// </summary>
/// <param name="signedData">The signed data.</param>
/// <param name="signature">The signature.</param>
/// <param name="symmetricSecretHandle">The symmetric secret handle. <c>null</c> when using an asymmetric algorithm.</param>
/// <returns>
/// <c>true</c> if the signature is valid; otherwise, <c>false</c>.
/// </returns>
private bool IsSignatureValid(byte[] signedData, byte[] signature, string symmetricSecretHandle) {
Requires.NotNull(signedData, "signedData");
Requires.NotNull(signature, "signature");
if (this.asymmetricSigning != null) {
using (var hasher = new SHA1CryptoServiceProvider()) {
return this.asymmetricSigning.VerifyData(signedData, hasher, signature);
}
} else {
return MessagingUtilities.AreEquivalentConstantTime(signature, this.CalculateSignature(signedData, symmetricSecretHandle));
}
}
/// <summary>
/// Calculates the signature for the data in this verification code.
/// </summary>
/// <param name="bytesToSign">The bytes to sign.</param>
/// <param name="symmetricSecretHandle">The symmetric secret handle. <c>null</c> when using an asymmetric algorithm.</param>
/// <returns>
/// The calculated signature.
/// </returns>
private byte[] CalculateSignature(byte[] bytesToSign, string symmetricSecretHandle) {
Requires.NotNull(bytesToSign, "bytesToSign");
Requires.ValidState(this.asymmetricSigning != null || this.cryptoKeyStore != null);
Contract.Ensures(Contract.Result<byte[]>() != null);
if (this.asymmetricSigning != null) {
using (var hasher = new SHA1CryptoServiceProvider()) {
return this.asymmetricSigning.SignData(bytesToSign, hasher);
}
} else {
var key = this.cryptoKeyStore.GetKey(this.cryptoKeyBucket, symmetricSecretHandle);
ErrorUtilities.VerifyProtocol(key != null, "Missing decryption key.");
using (var symmetricHasher = new HMACSHA256(key.Key)) {
return symmetricHasher.ComputeHash(bytesToSign);
}
}
}
/// <summary>
/// Encrypts the specified value using either the symmetric or asymmetric encryption algorithm as appropriate.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="symmetricSecretHandle">Receives the symmetric secret handle. <c>null</c> when using an asymmetric algorithm.</param>
/// <returns>
/// The encrypted value.
/// </returns>
private byte[] Encrypt(byte[] value, out string symmetricSecretHandle) {
Requires.ValidState(this.asymmetricEncrypting != null || this.cryptoKeyStore != null);
if (this.asymmetricEncrypting != null) {
symmetricSecretHandle = null;
return this.asymmetricEncrypting.EncryptWithRandomSymmetricKey(value);
} else {
var cryptoKey = this.cryptoKeyStore.GetCurrentKey(this.cryptoKeyBucket, this.minimumAge);
symmetricSecretHandle = cryptoKey.Key;
return MessagingUtilities.Encrypt(value, cryptoKey.Value.Key);
}
}
/// <summary>
/// Decrypts the specified value using either the symmetric or asymmetric encryption algorithm as appropriate.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="symmetricSecretHandle">The symmetric secret handle. <c>null</c> when using an asymmetric algorithm.</param>
/// <returns>
/// The decrypted value.
/// </returns>
private byte[] Decrypt(byte[] value, string symmetricSecretHandle) {
Requires.ValidState(this.asymmetricEncrypting != null || symmetricSecretHandle != null);
if (this.asymmetricEncrypting != null) {
return this.asymmetricEncrypting.DecryptWithRandomSymmetricKey(value);
} else {
var key = this.cryptoKeyStore.GetKey(this.cryptoKeyBucket, symmetricSecretHandle);
ErrorUtilities.VerifyProtocol(key != null, "Missing decryption key.");
return MessagingUtilities.Decrypt(value, key.Key);
}
}
}
}
|