/*
 * Protocolv20Decoder.java 
 * Created on 2011-07-21
 *
 * Copyright (c) Verax Systems 2011.
 * All rights reserved.
 *
 * This software is furnished under a license. Use, duplication,
 * disclosure and all other uses are restricted to the rights
 * specified in the written license agreement.
 */
package com.veraxsystems.vxipmi.coding.protocol.decoder;

import java.security.InvalidKeyException;
import java.util.Arrays;

import org.apache.log4j.Logger;

import com.veraxsystems.vxipmi.coding.protocol.AuthenticationType;
import com.veraxsystems.vxipmi.coding.protocol.IpmiMessage;
import com.veraxsystems.vxipmi.coding.protocol.Ipmiv20Message;
import com.veraxsystems.vxipmi.coding.protocol.PayloadType;
import com.veraxsystems.vxipmi.coding.rmcp.RmcpMessage;
import com.veraxsystems.vxipmi.coding.security.CipherSuite;
import com.veraxsystems.vxipmi.coding.security.ConfidentialityNone;
import com.veraxsystems.vxipmi.common.TypeConverter;

/**
 * Decodes IPMI v2.0 session header and retrieves encrypted payload.
 */
public class Protocolv20Decoder extends ProtocolDecoder {

    private static Logger logger = Logger.getLogger(Protocolv20Decoder.class);
    
	private CipherSuite cipherSuite;

	/**
	 * Initiates IPMI v2.0 packet decoder.
	 * 
	 * @param cipherSuite
	 *            - {@link CipherSuite} that will be used to decode the message
	 */
	public Protocolv20Decoder(CipherSuite cipherSuite) {
		super();
		this.cipherSuite = cipherSuite;
	}

	/**
	 * Decodes IPMI v2.0 message fields.
	 * 
	 * @param rmcpMessage
	 *            - RMCP message to decode.
	 * @return decoded message
	 * @see Ipmiv20Message
	 * @throws IllegalArgumentException
	 *             when delivered RMCP message does not contain encapsulated
	 *             IPMI message or when AuthCode field is incorrect (integrity
	 *             check fails).
	 * @throws InvalidKeyException
	 *             - when initiation of the integrity algorithm fails
	 */
	@Override
	public IpmiMessage decode(RmcpMessage rmcpMessage)
			throws IllegalArgumentException, InvalidKeyException {
		Ipmiv20Message message = new Ipmiv20Message(
				cipherSuite.getConfidentialityAlgorithm());

		byte[] raw = rmcpMessage.getData();

		message.setAuthenticationType(decodeAuthenticationType(raw[0]));

		message.setPayloadEncrypted(decodeEncryption(raw[1]));

		message.setPayloadAuthenticated(decodeAuthentication(raw[1]));

		message.setPayloadType(decodePayloadType(raw[1]));

		int offset = 2;

		if (message.getPayloadType() == PayloadType.Oem) {
			message.setOemIANA(decodeOEMIANA(raw));
			offset += 4;

			message.setOemPayloadID(decodeOEMPayloadId(raw, offset));
			offset += 2;
		}

		message.setSessionID(decodeSessionID(raw, offset));
		offset += 4;

		message.setSessionSequenceNumber(decodeSessionSequenceNumber(raw,
				offset));
		offset += 4;

		int payloadLength = decodePayloadLength(raw, offset);
		offset += 2;

		if (message.isPayloadEncrypted()) {
			message.setPayload(decodePayload(raw, offset, payloadLength,
					message.getConfidentialityAlgorithm()));
		} else {
			message.setPayload(decodePayload(raw, offset, payloadLength,
					new ConfidentialityNone()));
		}

		offset += payloadLength;

		if (message.getAuthenticationType() != AuthenticationType.None
				&& !(message.getAuthenticationType() == AuthenticationType.RMCPPlus && !message
						.isPayloadAuthenticated())
				&& message.getSessionID() != 0) {
			offset = skipIntegrityPAD(raw, offset);
			message.setAuthCode(decodeAuthCode(raw, offset));
			if (!validateAuthCode(raw, offset)) {
				logger.warn("Integrity check failed");
			}
		}

		return message;
	}

	/**
	 * Decodes first bit of Payload Type.
	 * 
	 * @param payloadType
	 * @return True if payload is encrypted, false otherwise.
	 */
	private boolean decodeEncryption(byte payloadType) {
		return !((payloadType & TypeConverter.intToByte(0x80)) == 0);
	}

	/**
	 * Decodes second bit of Payload Type.
	 * 
	 * @param payloadType
	 * @return True if payload is authenticated, false otherwise.
	 */
	public boolean decodeAuthentication(byte payloadType) {
		return !((payloadType & TypeConverter.intToByte(0x40)) == 0);
	}

	public static PayloadType decodePayloadType(byte payloadType)
			throws IllegalArgumentException {
		return PayloadType.parseInt(TypeConverter.intToByte(payloadType
				& TypeConverter.intToByte(0x3f)));
	}

	/**
	 * Decodes OEM IANA.
	 * 
	 * @param rawMessage
	 *            - Byte array holding whole message data.
	 * @return OEM IANA number.
	 */
	private int decodeOEMIANA(byte[] rawMessage) {
		byte[] oemIANA = new byte[4];

		System.arraycopy(rawMessage, 3, oemIANA, 0, 3);
		oemIANA[3] = 0;

		return TypeConverter.littleEndianByteArrayToInt(oemIANA);
	}

	/**
	 * Decodes OEM payload ID. To implement manufacturer-specific OEM Payload ID
	 * decoding, override this function.
	 * 
	 * @param rawMessage
	 *            - Byte array holding whole message data.
	 * @param offset
	 *            - Offset to OEM payload ID in header.
	 * @return Decoded OEM payload ID.
	 */
	protected Object decodeOEMPayloadId(byte[] rawMessage, int offset) {
		byte[] oemPayload = new byte[2];

		System.arraycopy(rawMessage, offset, oemPayload, 0, 2);

		return oemPayload;
	}

	@Override
	protected int decodePayloadLength(byte[] rawData, int offset) {
		byte[] payloadLength = new byte[4];
		System.arraycopy(rawData, offset, payloadLength, 0, 2);
		payloadLength[2] = 0;
		payloadLength[3] = 0;

		return TypeConverter.littleEndianByteArrayToInt(payloadLength);
	}

	/**
	 * Skips the integrity pad and pad length fields.
	 * 
	 * @param rawMessage
	 *            - Byte array holding whole message data.
	 * @param offset
	 *            - Offset to integrity pad.
	 * @return Offset to Auth Code
	 * @throws IndexOutOfBoundsException
	 *             when message is corrupted and pad length does not appear
	 *             after integrity pad or length is incorrect.
	 */
	private int skipIntegrityPAD(byte[] rawMessage, int offset)
			throws IndexOutOfBoundsException {
		int skip = 0;
		while (TypeConverter.byteToInt(rawMessage[offset + skip]) == 0xff) {
			++skip;
		}
		int length = TypeConverter.byteToInt(rawMessage[offset + skip]);
		if (length != skip) {
			throw new IndexOutOfBoundsException("Message is corrupted.");
		}
		offset += skip + 2; // skip pad length and next header fields
		if (offset >= rawMessage.length) {
			throw new IndexOutOfBoundsException("Message is corrupted.");
		}
		return offset;
	}

	/**
	 * Decodes the Auth Code.
	 * 
	 * @param rawMessage
	 *            - Byte array holding whole message data.
	 * @param offset
	 *            - Offset to auth code.
	 * @return Auth Code
	 * @throws IndexOutOfBoundsException
	 *             when message is corrupted and pad length does not appear
	 *             after integrity pad or length is incorrect.
	 */
	private byte[] decodeAuthCode(byte[] rawMessage, int offset) {
		byte[] authCode = new byte[rawMessage.length - offset];
		System.arraycopy(rawMessage, offset, authCode, 0, authCode.length);
		return authCode;
	}

	/**
	 * Checks if Auth Code of the received message is valid
	 * 
	 * @param rawMessage
	 *            - received message
	 * @param offset
	 *            - offset to the AuthCode field in the message
	 * @return True if AuthCode is correct, false otherwise.
	 * @throws InvalidKeyException
	 *             - when initiation of the integrity algorithm fails
	 */
	private boolean validateAuthCode(byte[] rawMessage, int offset)
			throws InvalidKeyException {
		byte[] base = new byte[offset];

		System.arraycopy(rawMessage, 0, base, 0, offset);

		byte[] authCode = null;

		if (rawMessage.length > offset) {
			authCode = new byte[rawMessage.length - offset];
			System.arraycopy(rawMessage, offset, authCode, 0, authCode.length);
		}

		return Arrays.equals(authCode, cipherSuite.getIntegrityAlgorithm()
				.generateAuthCode(base));
	}

	/**
	 * Decodes session ID.
	 * 
	 * @param message
	 *            - message to get session ID from
	 * @return Session ID.
	 */
	public static int decodeSessionID(RmcpMessage message) {
		int offset = 2;
		if (decodePayloadType(message.getData()[1]) == PayloadType.Oem) {
			offset += 6;
		}
		return decodeSessionID(message.getData(), offset);
	}
}