//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.Messaging {
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using DotNetOpenAuth.Logging;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.Messaging.Bindings;
using DotNetOpenAuth.Messaging.Reflection;
using Validation;
///
/// A serializer for -derived types
///
/// The DataBag-derived type that is to be serialized/deserialized.
internal abstract class DataBagFormatterBase : IDataBagFormatter where T : DataBag {
///
/// The message description cache to use for data bag types.
///
protected static readonly MessageDescriptionCollection MessageDescriptions = new MessageDescriptionCollection();
///
/// The length of the nonce to include in tokens that can be decoded once only.
///
private const int NonceLength = 6;
///
/// The minimum allowable lifetime for the key used to encrypt/decrypt or sign this databag.
///
private readonly TimeSpan minimumAge = TimeSpan.FromDays(1);
///
/// The symmetric key store with the secret used for signing/encryption of verification codes and refresh tokens.
///
private readonly ICryptoKeyStore cryptoKeyStore;
///
/// The bucket for symmetric keys.
///
private readonly string cryptoKeyBucket;
///
/// The crypto to use for signing access tokens.
///
private readonly RSACryptoServiceProvider asymmetricSigning;
///
/// The crypto to use for encrypting access tokens.
///
private readonly RSACryptoServiceProvider asymmetricEncrypting;
///
/// A value indicating whether the data in this instance will be protected against tampering.
///
private readonly bool signed;
///
/// The nonce store to use to ensure that this instance is only decoded once.
///
private readonly INonceStore decodeOnceOnly;
///
/// The maximum age of a token that can be decoded; useful only when is true.
///
private readonly TimeSpan? maximumAge;
///
/// A value indicating whether the data in this instance will be protected against eavesdropping.
///
private readonly bool encrypted;
///
/// A value indicating whether the data in this instance will be GZip'd.
///
private readonly bool compressed;
///
/// Initializes a new instance of the class.
///
/// The crypto service provider with the asymmetric key to use for signing or verifying the token.
/// The crypto service provider with the asymmetric key to use for encrypting or decrypting the token.
/// A value indicating whether the data in this instance will be GZip'd.
/// The maximum age of a token that can be decoded; useful only when is true.
/// The nonce store to use to ensure that this instance is only decoded once.
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;
}
///
/// Initializes a new instance of the class.
///
/// The crypto key store used when signing or encrypting.
/// The bucket in which symmetric keys are stored for signing/encrypting data.
/// A value indicating whether the data in this instance will be protected against tampering.
/// A value indicating whether the data in this instance will be protected against eavesdropping.
/// A value indicating whether the data in this instance will be GZip'd.
/// The required minimum lifespan within which this token must be decodable and verifiable; useful only when and/or is true.
/// The maximum age of a token that can be decoded; useful only when is true.
/// The nonce store to use to ensure that this instance is only decoded once.
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.That(!string.IsNullOrEmpty(bucket) || cryptoKeyStore == null, "bucket", "Bucket name required when cryptoKeyStore is non-null.");
Requires.That(cryptoKeyStore != null || (!signed && !encrypted), "cryptoKeyStore", "cryptoKeyStore required if signing or encrypting.");
this.cryptoKeyStore = cryptoKeyStore;
this.cryptoKeyBucket = bucket;
if (minimumAge.HasValue) {
this.minimumAge = minimumAge.Value;
}
}
///
/// Initializes a new instance of the class.
///
/// A value indicating whether the data in this instance will be protected against tampering.
/// A value indicating whether the data in this instance will be protected against eavesdropping.
/// A value indicating whether the data in this instance will be GZip'd.
/// The maximum age of a token that can be decoded; useful only when is true.
/// The nonce store to use to ensure that this instance is only decoded once.
private DataBagFormatterBase(bool signed = false, bool encrypted = false, bool compressed = false, TimeSpan? maximumAge = null, INonceStore decodeOnceOnly = null) {
Requires.That(signed || decodeOnceOnly == null, "decodeOnceOnly", "Nonce only valid with signing.");
Requires.That(maximumAge.HasValue || decodeOnceOnly == null, "decodeOnceOnly", "Nonce requires a maximum message age.");
this.signed = signed;
this.maximumAge = maximumAge;
this.decodeOnceOnly = decodeOnceOnly;
this.encrypted = encrypted;
this.compressed = compressed;
}
///
/// Serializes the specified message, including compression, encryption, signing, and nonce handling where applicable.
///
/// The message to serialize. Must not be null.
/// A non-null, non-empty value.
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No apparent problem. False positive?")]
public string Serialize(T message) {
Requires.NotNull(message, "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;
}
}
///
/// Deserializes a , including decompression, decryption, signature and nonce validation where applicable.
///
/// The instance to initialize with deserialized data.
/// The serialized form of the to deserialize. Must not be null or empty.
/// The message that contains the serialized value. May be null if no carrying message is applicable.
/// The name of the parameter whose value is to be deserialized. Used for error message generation, but may be null.
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No apparent problem. False positive?")]
public void Deserialize(T message, string value, IProtocolMessage containingMessage, string messagePartName) {
Requires.NotNull(message, "message");
Requires.NotNullOrEmpty(value, "value");
string symmetricSecretHandle = null;
if (this.encrypted && this.cryptoKeyStore != null) {
string valueWithoutHandle;
MessagingUtilities.ExtractKeyHandleAndPayload(messagePartName, value, out symmetricSecretHandle, out valueWithoutHandle);
value = valueWithoutHandle;
}
message.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(1024);
data = dataReader.ReadBuffer(8 * 1024);
}
// 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();
}
///
/// Serializes the instance to a buffer.
///
/// The message.
/// The buffer containing the serialized data.
protected abstract byte[] SerializeCore(T message);
///
/// Deserializes the instance from a buffer.
///
/// The message instance to initialize with data from the buffer.
/// The data buffer.
protected abstract void DeserializeCore(T message, byte[] data);
///
/// Determines whether the signature on this instance is valid.
///
/// The signed data.
/// The signature.
/// The symmetric secret handle. null when using an asymmetric algorithm.
///
/// true if the signature is valid; otherwise, false.
///
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No apparent problem. False positive?")]
private bool IsSignatureValid(byte[] signedData, byte[] signature, string symmetricSecretHandle) {
Requires.NotNull(signedData, "signedData");
Requires.NotNull(signature, "signature");
if (this.asymmetricSigning != null) {
using (var hasher = SHA1.Create()) {
return this.asymmetricSigning.VerifyData(signedData, hasher, signature);
}
} else {
return MessagingUtilities.AreEquivalentConstantTime(signature, this.CalculateSignature(signedData, symmetricSecretHandle));
}
}
///
/// Calculates the signature for the data in this verification code.
///
/// The bytes to sign.
/// The symmetric secret handle. null when using an asymmetric algorithm.
///
/// The calculated signature.
///
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No apparent problem. False positive?")]
private byte[] CalculateSignature(byte[] bytesToSign, string symmetricSecretHandle) {
Requires.NotNull(bytesToSign, "bytesToSign");
RequiresEx.ValidState(this.asymmetricSigning != null || this.cryptoKeyStore != null);
if (this.asymmetricSigning != null) {
using (var hasher = SHA1.Create()) {
return this.asymmetricSigning.SignData(bytesToSign, hasher);
}
} else {
var key = this.cryptoKeyStore.GetKey(this.cryptoKeyBucket, symmetricSecretHandle);
ErrorUtilities.VerifyProtocol(key != null, MessagingStrings.MissingDecryptionKeyForHandle, this.cryptoKeyBucket, symmetricSecretHandle);
using (var symmetricHasher = HmacAlgorithms.Create(HmacAlgorithms.HmacSha256, key.Key)) {
return symmetricHasher.ComputeHash(bytesToSign);
}
}
}
///
/// Encrypts the specified value using either the symmetric or asymmetric encryption algorithm as appropriate.
///
/// The value.
/// Receives the symmetric secret handle. null when using an asymmetric algorithm.
///
/// The encrypted value.
///
private byte[] Encrypt(byte[] value, out string symmetricSecretHandle) {
Assumes.True(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);
}
}
///
/// Decrypts the specified value using either the symmetric or asymmetric encryption algorithm as appropriate.
///
/// The value.
/// The symmetric secret handle. null when using an asymmetric algorithm.
///
/// The decrypted value.
///
private byte[] Decrypt(byte[] value, string symmetricSecretHandle) {
RequiresEx.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, MessagingStrings.MissingDecryptionKeyForHandle, this.cryptoKeyBucket, symmetricSecretHandle);
return MessagingUtilities.Decrypt(value, key.Key);
}
}
}
}