//----------------------------------------------------------------------- // // 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); } } } }