//
// Mono.Security.Cryptography.SymmetricTransform implementation
//
// Authors:
//	Thomas Neidhart (tome@sbox.tugraz.at)
//	Sebastien Pouliot <sebastien@ximian.com>
//
// Portions (C) 2002, 2003 Motus Technologies Inc. (http://www.motus.com)
// Copyright (C) 2004-2008 Novell, Inc (http://www.novell.com)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

using System;
using System.Security.Cryptography;

namespace Mono.Security.Cryptography {

	// This class implement most of the common code required for symmetric
	// algorithm transforms, like:
	// - CipherMode: Builds CBC and CFB on top of (descendant supplied) ECB
	// - PaddingMode, transform properties, multiple blocks, reuse...
	//
	// Descendants MUST:
	// - intialize themselves (like key expansion, ...)
	// - override the ECB (Electronic Code Book) method which will only be
	//   called using BlockSize byte[] array.
	internal abstract class SymmetricTransform : ICryptoTransform {
		protected SymmetricAlgorithm algo;
		protected bool encrypt;
		protected int BlockSizeByte;
		protected byte[] temp;
		protected byte[] temp2;
		private byte[] workBuff;
		private byte[] workout;
		protected PaddingMode padmode;
		// Silverlight 2.0 does not support any feedback mode
		protected int FeedBackByte;
		private bool m_disposed = false;
		protected bool lastBlock;

		public SymmetricTransform (SymmetricAlgorithm symmAlgo, bool encryption, byte[] rgbIV) 
		{
			algo = symmAlgo;
			encrypt = encryption;
			BlockSizeByte = (algo.BlockSize >> 3);

			if (rgbIV == null) {
				rgbIV = KeyBuilder.IV (BlockSizeByte);
			} else {
				rgbIV = (byte[]) rgbIV.Clone ();
			}
			// compare the IV length with the "currently selected" block size and *ignore* IV that are too big
			if (rgbIV.Length < BlockSizeByte) {
				string msg = Locale.GetText ("IV is too small ({0} bytes), it should be {1} bytes long.",
					rgbIV.Length, BlockSizeByte);
				throw new CryptographicException (msg);
			}
			padmode = algo.Padding;
			// mode buffers
			temp = new byte [BlockSizeByte];
			Buffer.BlockCopy (rgbIV, 0, temp, 0, System.Math.Min (BlockSizeByte, rgbIV.Length));
			temp2 = new byte [BlockSizeByte];
			FeedBackByte = (algo.FeedbackSize >> 3);
			// transform buffers
			workBuff = new byte [BlockSizeByte];
			workout =  new byte [BlockSizeByte];
		}

		~SymmetricTransform () 
		{
			Dispose (false);
		}

		void IDisposable.Dispose () 
		{
			Dispose (true);
			GC.SuppressFinalize (this);  // Finalization is now unnecessary
		}

		// MUST be overriden by classes using unmanaged ressources
		// the override method must call the base class
		protected virtual void Dispose (bool disposing) 
		{
			if (!m_disposed) {
				if (disposing) {
					// dispose managed object: zeroize and free
					Array.Clear (temp, 0, BlockSizeByte);
					temp = null;
					Array.Clear (temp2, 0, BlockSizeByte);
					temp2 = null;
				}
				m_disposed = true;
			}
		}

		public virtual bool CanTransformMultipleBlocks {
			get { return true; }
		}

		public virtual bool CanReuseTransform {
			get { return false; }
		}

		public virtual int InputBlockSize {
			get { return BlockSizeByte; }
		}

		public virtual int OutputBlockSize {
			get { return BlockSizeByte; }
		}

		// note: Each block MUST be BlockSizeValue in size!!!
		// i.e. Any padding must be done before calling this method
		protected virtual void Transform (byte[] input, byte[] output) 
		{
			switch (algo.Mode) {
			case CipherMode.ECB:
				ECB (input, output);
				break;
			case CipherMode.CBC:
				CBC (input, output);
				break;
			case CipherMode.CFB:
				CFB (input, output);
				break;
			case CipherMode.OFB:
				OFB (input, output);
				break;
			case CipherMode.CTS:
				CTS (input, output);
				break;
			default:
				throw new NotImplementedException ("Unkown CipherMode" + algo.Mode.ToString ());
			}
		}

		// Electronic Code Book (ECB)
		protected abstract void ECB (byte[] input, byte[] output); 

		// Cipher-Block-Chaining (CBC)
		protected virtual void CBC (byte[] input, byte[] output) 
		{
			if (encrypt) {
				for (int i = 0; i < BlockSizeByte; i++)
					temp[i] ^= input[i];
				ECB (temp, output);
				Buffer.BlockCopy (output, 0, temp, 0, BlockSizeByte);
			}
			else {
				Buffer.BlockCopy (input, 0, temp2, 0, BlockSizeByte);
				ECB (input, output);
				for (int i = 0; i < BlockSizeByte; i++)
					output[i] ^= temp[i];
				Buffer.BlockCopy (temp2, 0, temp, 0, BlockSizeByte);
			}
		}

		// Cipher-FeedBack (CFB)
		// this is how *CryptoServiceProvider implements CFB
		// only AesCryptoServiceProvider support CFB > 8
		// RijndaelManaged is incompatible with this implementation (and overrides it in it's own transform)
		protected virtual void CFB (byte[] input, byte[] output) 
		{
			if (encrypt) {
				for (int x = 0; x < BlockSizeByte; x++) {
					// temp is first initialized with the IV
					ECB (temp, temp2);
					output [x] = (byte) (temp2 [0] ^ input [x]);
					Buffer.BlockCopy (temp, 1, temp, 0, BlockSizeByte - 1);
					Buffer.BlockCopy (output, x, temp, BlockSizeByte - 1, 1);
				}
			}
			else {
				for (int x = 0; x < BlockSizeByte; x++) {
					// we do not really decrypt this data!
					encrypt = true;
					// temp is first initialized with the IV
					ECB (temp, temp2);
					encrypt = false;

					Buffer.BlockCopy (temp, 1, temp, 0, BlockSizeByte - 1);
					Buffer.BlockCopy (input, x, temp, BlockSizeByte - 1, 1);
					output [x] = (byte) (temp2 [0] ^ input [x]);
				}
			}
		}

		// Output-FeedBack (OFB)
		protected virtual void OFB (byte[] input, byte[] output) 
		{
			throw new CryptographicException ("OFB isn't supported by the framework");
		}

		// Cipher Text Stealing (CTS)
		protected virtual void CTS (byte[] input, byte[] output) 
		{
			throw new CryptographicException ("CTS isn't supported by the framework");
		}

		private void CheckInput (byte[] inputBuffer, int inputOffset, int inputCount)
		{
			if (inputBuffer == null)
				throw new ArgumentNullException ("inputBuffer");
			if (inputOffset < 0)
				throw new ArgumentOutOfRangeException ("inputOffset", "< 0");
			if (inputCount < 0)
				throw new ArgumentOutOfRangeException ("inputCount", "< 0");
			// ordered to avoid possible integer overflow
			if (inputOffset > inputBuffer.Length - inputCount)
				throw new ArgumentException ("inputBuffer", Locale.GetText ("Overflow"));
		}

		// this method may get called MANY times so this is the one to optimize
		public virtual int TransformBlock (byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) 
		{
			if (m_disposed)
				throw new ObjectDisposedException ("Object is disposed");
			CheckInput (inputBuffer, inputOffset, inputCount);
			// check output parameters
			if (outputBuffer == null)
				throw new ArgumentNullException ("outputBuffer");
			if (outputOffset < 0)
				throw new ArgumentOutOfRangeException ("outputOffset", "< 0");

			// ordered to avoid possible integer overflow
			int len = outputBuffer.Length - inputCount - outputOffset;
			if (!encrypt && (0 > len) && ((padmode == PaddingMode.None) || (padmode == PaddingMode.Zeros))) {
				throw new CryptographicException ("outputBuffer", Locale.GetText ("Overflow"));
			} else if (KeepLastBlock) {
				if (0 > len + BlockSizeByte) {
					throw new CryptographicException ("outputBuffer", Locale.GetText ("Overflow"));
				}
			} else {
				if (0 > len) {
					// there's a special case if this is the end of the decryption process
					if (inputBuffer.Length - inputOffset - outputBuffer.Length == BlockSizeByte)
						inputCount = outputBuffer.Length - outputOffset;
					else
						throw new CryptographicException ("outputBuffer", Locale.GetText ("Overflow"));
				}
			}
			return InternalTransformBlock (inputBuffer, inputOffset, inputCount, outputBuffer, outputOffset);
		}

		private bool KeepLastBlock {
			get {
				return ((!encrypt) && (padmode != PaddingMode.None) && (padmode != PaddingMode.Zeros));
			}
		}

		private int InternalTransformBlock (byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) 
		{
			int offs = inputOffset;
			int full;

			// this way we don't do a modulo every time we're called
			// and we may save a division
			if (inputCount != BlockSizeByte) {
				if ((inputCount % BlockSizeByte) != 0)
					throw new CryptographicException ("Invalid input block size.");

				full = inputCount / BlockSizeByte;
			}
			else
				full = 1;

			if (KeepLastBlock)
				full--;

			int total = 0;

			if (lastBlock) {
				Transform (workBuff, workout);
				Buffer.BlockCopy (workout, 0, outputBuffer, outputOffset, BlockSizeByte);
				outputOffset += BlockSizeByte;
				total += BlockSizeByte;
				lastBlock = false;
			}

			for (int i = 0; i < full; i++) {
				Buffer.BlockCopy (inputBuffer, offs, workBuff, 0, BlockSizeByte);
				Transform (workBuff, workout);
				Buffer.BlockCopy (workout, 0, outputBuffer, outputOffset, BlockSizeByte);
				offs += BlockSizeByte;
				outputOffset += BlockSizeByte;
				total += BlockSizeByte;
			}

			if (KeepLastBlock) {
				Buffer.BlockCopy (inputBuffer, offs, workBuff, 0, BlockSizeByte);
				lastBlock = true;
			}

			return total;
		}

		RandomNumberGenerator _rng;

		private void Random (byte[] buffer, int start, int length)
		{
			if (_rng == null) {
				_rng = RandomNumberGenerator.Create ();
			}
			byte[] random = new byte [length];
			_rng.GetBytes (random);
			Buffer.BlockCopy (random, 0, buffer, start, length);
		}

		private void ThrowBadPaddingException (PaddingMode padding, int length, int position)
		{
			string msg = String.Format (Locale.GetText ("Bad {0} padding."), padding);
			if (length >= 0)
				msg += String.Format (Locale.GetText (" Invalid length {0}."), length);
			if (position >= 0)
				msg += String.Format (Locale.GetText (" Error found at position {0}."), position);
			throw new CryptographicException (msg);
		}

		protected virtual byte[] FinalEncrypt (byte[] inputBuffer, int inputOffset, int inputCount) 
		{
			// are there still full block to process ?
			int full = (inputCount / BlockSizeByte) * BlockSizeByte;
			int rem = inputCount - full;
			int total = full;

			switch (padmode) {
			case PaddingMode.ANSIX923:
			case PaddingMode.ISO10126:
			case PaddingMode.PKCS7:
				// we need to add an extra block for padding
				total += BlockSizeByte;
				break;
			default:
				if (inputCount == 0)
					return new byte [0];
				if (rem != 0) {
					if (padmode == PaddingMode.None)
						throw new CryptographicException ("invalid block length");
					// zero padding the input (by adding a block for the partial data)
					byte[] paddedInput = new byte [full + BlockSizeByte];
					Buffer.BlockCopy (inputBuffer, inputOffset, paddedInput, 0, inputCount);
					inputBuffer = paddedInput;
					inputOffset = 0;
					inputCount = paddedInput.Length;
					total = inputCount;
				}
				break;
			}

			byte[] res = new byte [total];
			int outputOffset = 0;

			// process all blocks except the last (final) block
			while (total > BlockSizeByte) {
				InternalTransformBlock (inputBuffer, inputOffset, BlockSizeByte, res, outputOffset);
				inputOffset += BlockSizeByte;
				outputOffset += BlockSizeByte;
				total -= BlockSizeByte;
			}

			// now we only have a single last block to encrypt
			byte padding = (byte) (BlockSizeByte - rem);
			switch (padmode) {
			case PaddingMode.ANSIX923:
				// XX 00 00 00 00 00 00 07 (zero + padding length)
				res [res.Length - 1] = padding;
				Buffer.BlockCopy (inputBuffer, inputOffset, res, full, rem);
				// the last padded block will be transformed in-place
				InternalTransformBlock (res, full, BlockSizeByte, res, full);
				break;
			case PaddingMode.ISO10126:
				// XX 3F 52 2A 81 AB F7 07 (random + padding length)
				Random (res, res.Length - padding, padding - 1);
				res [res.Length - 1] = padding;
				Buffer.BlockCopy (inputBuffer, inputOffset, res, full, rem);
				// the last padded block will be transformed in-place
				InternalTransformBlock (res, full, BlockSizeByte, res, full);
				break;
			case PaddingMode.PKCS7:
				// XX 07 07 07 07 07 07 07 (padding length)
				for (int i = res.Length; --i >= (res.Length - padding);) 
					res [i] = padding;
				Buffer.BlockCopy (inputBuffer, inputOffset, res, full, rem);
				// the last padded block will be transformed in-place
				InternalTransformBlock (res, full, BlockSizeByte, res, full);
				break;
			default:
				InternalTransformBlock (inputBuffer, inputOffset, BlockSizeByte, res, outputOffset);
				break;
			}
			return res;
		}

		protected virtual byte[] FinalDecrypt (byte[] inputBuffer, int inputOffset, int inputCount) 
		{
			int full = inputCount;
			int total = inputCount;
			if (lastBlock)
				total += BlockSizeByte;

			byte[] res = new byte [total];
			int outputOffset = 0;

			while (full > 0) {
				int len = InternalTransformBlock (inputBuffer, inputOffset, BlockSizeByte, res, outputOffset);
				inputOffset += BlockSizeByte;
				outputOffset += len;
				full -= BlockSizeByte;
			}

			if (lastBlock) {
				Transform (workBuff, workout);
				Buffer.BlockCopy (workout, 0, res, outputOffset, BlockSizeByte);
				outputOffset += BlockSizeByte;
				lastBlock = false;
			}

			// total may be 0 (e.g. PaddingMode.None)
			byte padding = ((total > 0) ? res [total - 1] : (byte) 0);
			switch (padmode) {
			case PaddingMode.ANSIX923:
				if ((padding == 0) || (padding > BlockSizeByte))
					ThrowBadPaddingException (padmode, padding, -1);
				for (int i = padding - 1; i > 0; i--) {
					if (res [total - 1 - i] != 0x00)
						ThrowBadPaddingException (padmode, -1, i);
				}
				total -= padding;
				break;
			case PaddingMode.ISO10126:
				if ((padding == 0) || (padding > BlockSizeByte))
					ThrowBadPaddingException (padmode, padding, -1);
				total -= padding;
				break;
			case PaddingMode.PKCS7:
				if ((padding == 0) || (padding > BlockSizeByte))
					ThrowBadPaddingException (padmode, padding, -1);
				for (int i = padding - 1; i > 0; i--) {
					if (res [total - 1 - i] != padding)
						ThrowBadPaddingException (padmode, -1, i);
				}
				total -= padding;
				break;
			case PaddingMode.None:	// nothing to do - it's a multiple of block size
			case PaddingMode.Zeros:	// nothing to do - user must unpad himself
				break;
			}

			// return output without padding
			if (total > 0) {
				byte[] data = new byte [total];
				Buffer.BlockCopy (res, 0, data, 0, total);
				// zeroize decrypted data (copy with padding)
				Array.Clear (res, 0, res.Length);
				return data;
			}
			else
				return new byte [0];
		}

		public virtual byte[] TransformFinalBlock (byte[] inputBuffer, int inputOffset, int inputCount) 
		{
			if (m_disposed)
				throw new ObjectDisposedException ("Object is disposed");
			CheckInput (inputBuffer, inputOffset, inputCount);

			if (encrypt)
				return FinalEncrypt (inputBuffer, inputOffset, inputCount);
			else
				return FinalDecrypt (inputBuffer, inputOffset, inputCount);
		}
	}
}
