/******************************************************************************* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"). You may not use * this file except in compliance with the License. A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * ***************************************************************************** * __ _ _ ___ * ( )( \/\/ )/ __) * /__\ \ / \__ \ * (_)(_) \/\/ (___/ * * AWS SDK for .NET * API Version: 2006-03-01 * */ using Amazon.Util.Internal; using System; using System.IO; #if AWS_ASYNC_API using System.Threading; using System.Threading.Tasks; #endif namespace Amazon.Runtime.Internal.Util { /// /// A wrapper stream that calculates a hash of the base stream as it /// is being read. /// The calculated hash is only available after the stream is closed or /// CalculateHash is called. After calling CalculateHash, any further reads /// on the streams will not change the CalculatedHash. /// If an ExpectedHash is specified and is not equal to the calculated hash, /// Close or CalculateHash methods will throw an AmazonClientException. /// If CalculatedHash is calculated for only the portion of the stream that /// is read. /// /// /// Exception thrown during Close() or CalculateHash(), if ExpectedHash is set and /// is different from CalculateHash that the stream calculates, provided that /// CalculatedHash is not a zero-length byte array. /// public abstract class HashStream : WrapperStream { #region Properties /// /// Algorithm to use to calculate hash. /// protected IHashingWrapper Algorithm { get; set; } /// /// True if hashing is finished and no more hashing should be done; /// otherwise false. /// protected bool FinishedHashing { get { return CalculatedHash != null; } } /// /// Current position in the stream. /// protected long CurrentPosition { get; private set; } /// /// Calculated hash for the stream. /// This value is set only after the stream is closed. /// public byte[] CalculatedHash { get; protected set; } /// /// Expected hash value. Compared against CalculatedHash upon Close(). /// If the hashes are different, an AmazonClientException is thrown. /// public byte[] ExpectedHash { get; private set; } /// /// Expected length of stream. /// public long ExpectedLength { get; protected set; } #endregion #region Constructors ///// ///// Initializes an HashStream with a hash algorithm and a base stream. ///// ///// Stream to calculate hash for. //protected HashStream(Stream baseStream) // : this(baseStream, null) { } /// /// Initializes an HashStream with a hash algorithm and a base stream. /// /// Stream to calculate hash for. /// /// Expected hash. Will be compared against calculated hash on stream close. /// Pass in null to disable check. /// /// /// Expected length of the stream. If the reading stops before reaching this /// position, CalculatedHash will be set to empty array. /// protected HashStream(Stream baseStream, byte[] expectedHash, long expectedLength) : base(baseStream) { ExpectedHash = expectedHash; ExpectedLength = expectedLength; ValidateBaseStream(); Reset(); } #endregion #region Stream overrides /// /// Reads a sequence of bytes from the current stream and advances the position /// within the stream by the number of bytes read. /// /// /// An array of bytes. When this method returns, the buffer contains the specified /// byte array with the values between offset and (offset + count - 1) replaced /// by the bytes read from the current source. /// /// /// The zero-based byte offset in buffer at which to begin storing the data read /// from the current stream. /// /// /// The maximum number of bytes to be read from the current stream. /// /// /// The total number of bytes read into the buffer. This can be less than the /// number of bytes requested if that many bytes are not currently available, /// or zero (0) if the end of the stream has been reached. /// public override int Read(byte[] buffer, int offset, int count) { int result = base.Read(buffer, offset, count); CurrentPosition += result; if (!FinishedHashing) { Algorithm.AppendBlock(buffer, offset, result); } // Calculate the hash if this was the final read if (result == 0) { CalculateHash(); } return result; } #if AWS_ASYNC_API /// /// Asynchronously reads a sequence of bytes from the current stream, advances /// the position within the stream by the number of bytes read, and monitors /// cancellation requests. /// /// /// An array of bytes. When this method returns, the buffer contains the specified /// byte array with the values between offset and (offset + count - 1) replaced /// by the bytes read from the current source. /// /// /// The zero-based byte offset in buffer at which to begin storing the data read /// from the current stream. /// /// /// The maximum number of bytes to be read from the current stream. /// /// /// The token to monitor for cancellation requests. The default value is /// System.Threading.CancellationToken.None. /// /// /// A task that represents the asynchronous read operation. The value of the TResult /// parameter contains the total number of bytes read into the buffer. This can be /// less than the number of bytes requested if that many bytes are not currently /// available, or zero (0) if the end of the stream has been reached. /// public async override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { int result = await base.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); CurrentPosition += result; if (!FinishedHashing) { Algorithm.AppendBlock(buffer, offset, result); } // Calculate the hash if this was the final read if (result == 0) { CalculateHash(); } return result; } #endif #if !NETSTANDARD /// /// Closes the underlying stream and finishes calculating the hash. /// If an ExpectedHash is specified and is not equal to the calculated hash, /// this method will throw an AmazonClientException. /// /// /// If ExpectedHash is set and is different from CalculateHash that the stream calculates. /// public override void Close() { CalculateHash(); base.Close(); } #endif protected override void Dispose(bool disposing) { try { CalculateHash(); if (disposing && Algorithm != null) { Algorithm.Dispose(); Algorithm = null; } } finally { base.Dispose(disposing); } } /// /// Gets a value indicating whether the current stream supports seeking. /// HashStream does not support seeking, this will always be false. /// public override bool CanSeek { get { // Restrict random access, as this will break hashing. return false; } } /// /// Gets or sets the position within the current stream. /// HashStream does not support seeking, attempting to set Position /// will throw NotSupportedException. /// public override long Position { get { throw new NotSupportedException("HashStream does not support seeking"); } set { // Restrict random access, as this will break hashing. throw new NotSupportedException("HashStream does not support seeking"); } } /// /// Sets the position within the current stream. /// HashStream does not support seeking, attempting to call Seek /// will throw NotSupportedException. /// /// A byte offset relative to the origin parameter. /// /// A value of type System.IO.SeekOrigin indicating the reference point used /// to obtain the new position. /// The new position within the current stream. public override long Seek(long offset, SeekOrigin origin) { // Restrict random access, as this will break hashing. throw new NotSupportedException("HashStream does not support seeking"); } /// /// Gets the overridden length used to construct the HashStream /// public override long Length { get { return this.ExpectedLength; } } #endregion #region Public methods /// /// Calculates the hash for the stream so far and disables any further /// hashing. /// public virtual void CalculateHash() { if (!FinishedHashing) { if (ExpectedLength < 0 || CurrentPosition == ExpectedLength) { CalculatedHash = Algorithm.AppendLastBlock(ArrayEx.Empty()); } else CalculatedHash = ArrayEx.Empty(); if (CalculatedHash.Length > 0 && ExpectedHash != null && ExpectedHash.Length > 0) { if (!CompareHashes(ExpectedHash, CalculatedHash)) throw new AmazonClientException("Expected hash not equal to calculated hash"); } } } /// /// Resets the hash stream to starting state. /// Use this if the underlying stream has been modified and needs /// to be rehashed without reconstructing the hierarchy. /// public void Reset() { CurrentPosition = 0; CalculatedHash = null; if (Algorithm != null) Algorithm.Clear(); var baseHashStream = BaseStream as HashStream; if (baseHashStream != null) { baseHashStream.Reset(); } } #endregion #region Private methods /// /// Validates the underlying stream. /// private void ValidateBaseStream() { // Fast-fail on unusable streams if (!BaseStream.CanRead && !BaseStream.CanWrite) throw new InvalidDataException("HashStream does not support base streams that are not capable of reading or writing"); } /// /// Compares two hashes (arrays of bytes). /// /// Expected hash. /// Actual hash. /// /// True if the hashes are identical; otherwise false. /// protected static bool CompareHashes(byte[] expected, byte[] actual) { if (ReferenceEquals(expected, actual)) return true; if (expected == null || actual == null) return (expected == actual); if (expected.Length != actual.Length) return false; for (int i = 0; i < expected.Length; i++) { if (expected[i] != actual[i]) return false; } return true; } #endregion } /// /// A wrapper stream that calculates a hash of the base stream as it /// is being read or written. /// The calculated hash is only available after the stream is closed or /// CalculateHash is called. After calling CalculateHash, any further reads /// on the streams will not change the CalculatedHash. /// If an ExpectedHash is specified and is not equal to the calculated hash, /// Close or CalculateHash methods will throw an AmazonClientException. /// If base stream's position is not 0 or HashOnReads is true and the entire stream is /// not read, the CalculatedHash will be set to an empty byte array and /// comparison to ExpectedHash will not be made. /// /// /// Exception thrown during Close() or CalculateHash(), if ExpectedHash is set and /// is different from CalculateHash that the stream calculates, provided that /// CalculatedHash is not a zero-length byte array. /// public class HashStream : HashStream where T : IHashingWrapper, new() { #region Constructors /// /// Initializes an HashStream with a hash algorithm and a base stream. /// /// Stream to calculate hash for. /// /// Expected hash. Will be compared against calculated hash on stream close. /// Pass in null to disable check. /// /// /// Expected length of the stream. If the reading stops before reaching this /// position, CalculatedHash will be set to empty array. /// public HashStream(Stream baseStream, byte[] expectedHash, long expectedLength) : base(baseStream, expectedHash, expectedLength) { Algorithm = new T(); } #endregion } /// /// A wrapper stream that calculates an MD5 hash of the base stream as it /// is being read or written. /// The calculated hash is only available after the stream is closed or /// CalculateHash is called. After calling CalculateHash, any further reads /// on the streams will not change the CalculatedHash. /// If an ExpectedHash is specified and is not equal to the calculated hash, /// Close or CalculateHash methods will throw an AmazonClientException. /// If base stream's position is not 0 or HashOnReads is true and the entire stream is /// not read, the CalculatedHash will be set to an empty byte array and /// comparison to ExpectedHash will not be made. /// /// /// Exception thrown during Close() or CalculateHash(), if ExpectedHash is set and /// is different from CalculateHash that the stream calculates, provided that /// CalculatedHash is not a zero-length byte array. /// public class MD5Stream : HashStream { private Logger _logger; #region Constructors /// /// Initializes an MD5Stream with a base stream. /// /// Stream to calculate hash for. /// /// Expected hash. Will be compared against calculated hash on stream close. /// Pass in null to disable check. /// /// /// Expected length of the stream. If the reading stops before reaching this /// position, CalculatedHash will be set to empty array. /// public MD5Stream(Stream baseStream, byte[] expectedHash, long expectedLength) : base(baseStream, expectedHash, expectedLength) { _logger = Logger.GetLogger(this.GetType()); } #endregion } }