// ==++==
// 
//   Copyright (c) Microsoft Corporation.  All rights reserved.
// 
// ==--==
// <OWNER>Microsoft</OWNER>
// 

//
// MACTripleDES.cs -- Implementation of the MAC-CBC keyed hash w/ 3DES
//

// See: http://www.itl.nist.gov/fipspubs/fip81.htm for a spec

namespace System.Security.Cryptography {
    using System.IO;
    using System.Diagnostics.Contracts;

    [System.Runtime.InteropServices.ComVisible(true)]
    public class MACTripleDES : KeyedHashAlgorithm 
    {
        // Output goes to HashMemorySink since we don't care about the actual data
        private ICryptoTransform m_encryptor;
        private CryptoStream _cs;
        private TailStream _ts;
        private const int m_bitsPerByte = 8;
        private int m_bytesPerBlock;
        private TripleDES des;

        //
        // public constructors
        //

        public MACTripleDES() {
            KeyValue = new byte[24];
            Utils.StaticRandomNumberGenerator.GetBytes(KeyValue);

            // Create a TripleDES encryptor
            des = TripleDES.Create();
            HashSizeValue = des.BlockSize;

            m_bytesPerBlock = des.BlockSize/m_bitsPerByte;
            // By definition, MAC-CBC-3DES takes an IV=0.  C# zero-inits arrays,
            // so all we have to do here is define it.
            des.IV = new byte[m_bytesPerBlock];
            des.Padding = PaddingMode.Zeros;

            m_encryptor = null;
        }

        public MACTripleDES(byte[] rgbKey) 
            : this("System.Security.Cryptography.TripleDES",rgbKey) {}

        public MACTripleDES(String strTripleDES, byte[] rgbKey) {
            // Make sure we know which algorithm to use
            if (rgbKey == null)
                throw new ArgumentNullException("rgbKey");
            Contract.EndContractBlock();
            // Create a TripleDES encryptor
            if (strTripleDES == null) {
                des = TripleDES.Create();
            } else {
                des = TripleDES.Create(strTripleDES);
            }

            HashSizeValue = des.BlockSize;
            // Stash the key away
            KeyValue = (byte[]) rgbKey.Clone();

            m_bytesPerBlock = des.BlockSize/m_bitsPerByte;
            // By definition, MAC-CBC-3DES takes an IV=0.  C# zero-inits arrays,
            // so all we have to do here is define it.
            des.IV = new byte[m_bytesPerBlock];
            des.Padding = PaddingMode.Zeros;

            m_encryptor = null;
        }

        public override void Initialize() {
            m_encryptor = null;
        }

        [System.Runtime.InteropServices.ComVisible(false)]
        public PaddingMode Padding {
            get { return des.Padding; }
            set { 
                if ((value < PaddingMode.None) || (PaddingMode.ISO10126 < value))
                    throw new CryptographicException(Environment.GetResourceString("Cryptography_InvalidPaddingMode"));
                des.Padding = value;
            }
        }

        //  
        // protected methods
        //

        protected override void HashCore(byte[] rgbData, int ibStart, int cbSize) {
            // regenerate the TripleDES object before each call to ComputeHash
            if (m_encryptor == null) {
                des.Key = this.Key;
                m_encryptor = des.CreateEncryptor();
                _ts = new TailStream(des.BlockSize / 8); // 8 bytes
                _cs = new CryptoStream(_ts, m_encryptor, CryptoStreamMode.Write);
            }

            // Encrypt using 3DES
            _cs.Write(rgbData, ibStart, cbSize);
        }

        protected override byte[] HashFinal() {
            // If Hash has been called on a zero buffer
            if (m_encryptor == null) {
                des.Key = this.Key;
                m_encryptor = des.CreateEncryptor();
                _ts = new TailStream(des.BlockSize / 8); // 8 bytes 
                _cs = new CryptoStream(_ts, m_encryptor, CryptoStreamMode.Write);
            }

            // Finalize the hashing and return the result
            _cs.FlushFinalBlock();
            return _ts.Buffer;
        }

        // IDisposable methods
        protected override void Dispose(bool disposing) {
            if (disposing) {
                // dispose of our internal state
                if (des != null)
                    des.Clear();
                if (m_encryptor != null)
                    m_encryptor.Dispose();
                if (_cs != null)
                    _cs.Clear();
                if (_ts != null)
                    _ts.Clear();
            }
            base.Dispose(disposing);
        }
    }

    //
    // TailStream is another utility class -- it remembers the last n bytes written to it
    // This is useful for MAC-3DES since we need to capture only the result of the last block

    internal sealed class TailStream : Stream {
        private byte[] _Buffer;
        private int _BufferSize;
        private int _BufferIndex = 0;
        private bool _BufferFull = false;

        public TailStream(int bufferSize) {
            _Buffer = new byte[bufferSize];
            _BufferSize = bufferSize;
        }

        public void Clear() {
            Close();
        }

        protected override void Dispose(bool disposing) {
            try {
                if (disposing) {
                    if (_Buffer != null) {
                        Array.Clear(_Buffer, 0, _Buffer.Length);
                    }
                    _Buffer = null;
                }
            }
            finally {
                base.Dispose(disposing);
            }
        }

        public byte[] Buffer {
            get { return (byte[]) _Buffer.Clone(); }
        }

        public override bool CanRead {
            [Pure]
            get { return false; }
        }

        public override bool CanSeek {
            [Pure]
            get { return false; }
        }

        public override bool CanWrite {
            [Pure]
            get { return _Buffer != null; }
        }

        public override long Length {
            get { throw new NotSupportedException(Environment.GetResourceString("NotSupported_UnseekableStream")); }
        }

        public override long Position {
            get { throw new NotSupportedException(Environment.GetResourceString("NotSupported_UnseekableStream")); }
            set { throw new NotSupportedException(Environment.GetResourceString("NotSupported_UnseekableStream")); }
        }

        public override void Flush() {
            return;
        }

        public override long Seek(long offset, SeekOrigin origin) {
            throw new NotSupportedException(Environment.GetResourceString("NotSupported_UnseekableStream"));
        }

        public override void SetLength(long value) {
            throw new NotSupportedException(Environment.GetResourceString("NotSupported_UnseekableStream"));
        }

        public override int Read(byte[] buffer, int offset, int count) {
            throw new NotSupportedException(Environment.GetResourceString("NotSupported_UnreadableStream"));
        }

        public override void Write(byte[] buffer, int offset, int count) {
            if (_Buffer == null)
                throw new ObjectDisposedException("TailStream");

            // If no bytes to write, then return
            if (count == 0) return;
            // The most common case will be when we have a full buffer
            if (_BufferFull) {
                // if more bytes are written in this call than the size of the buffer,
                // just remember the last _BufferSize bytes
                if (count > _BufferSize) {
                    System.Buffer.InternalBlockCopy(buffer, offset+count-_BufferSize, _Buffer, 0, _BufferSize);
                    return;
                } else {
                    // move _BufferSize - count bytes left, then copy the new bytes
                    System.Buffer.InternalBlockCopy(_Buffer, _BufferSize - count, _Buffer, 0, _BufferSize - count);
                    System.Buffer.InternalBlockCopy(buffer, offset, _Buffer, _BufferSize - count, count);
                    return;
                }
            } else {
                // buffer isn't full yet, so more cases
                if (count > _BufferSize) {
                    System.Buffer.InternalBlockCopy(buffer, offset+count-_BufferSize, _Buffer, 0, _BufferSize);
                    _BufferFull = true;
                    return;
                } else if (count + _BufferIndex >= _BufferSize) {
                    System.Buffer.InternalBlockCopy(_Buffer, _BufferIndex+count-_BufferSize, _Buffer, 0, _BufferSize - count);
                    System.Buffer.InternalBlockCopy(buffer, offset, _Buffer, _BufferIndex, count);
                    _BufferFull = true;
                    return;
                } else {
                    System.Buffer.InternalBlockCopy(buffer, offset, _Buffer, _BufferIndex, count);
                    _BufferIndex += count;
                    return;
                }
            }
        }
    }
}
