package proof;

import sdsi.*;
import sdsi.sexp.*;
import Tools.*;

import java.util.*;
import java.security.*;

/**
 * An object speaks for the hash of a (secret) MAC (Message
 * Authentication Code, which term I'm
 * probably using incorrectly) if we can present a hash of strcat(the
 * object, the secret MAC). That would mean that the holder of the
 * secret MAC allowed its hash to be taken with the object; it's
 * essentially how one "signs" an object using a secret number.<p>
 *
 * This proof shows that an ObjectHash speaks for another ObjectHash,
 * where the first is the hash of (object,MAC) and the second is
 * hash of just the MAC. For .verify() to succeed, this proof needs
 * to be supplied in advance with a pointer to the object to hash
 * as well as the secret MAC. (These pointers are obviously not
 * transmitted with the object over the network.)<p>
 *
 * To verify proofs that depend on an instance of this class,
 * a server must supply in advance the appropriate MAC binding that
 * it accepts. That is, MAC signed requests are not self-evident like
 * a public key; they depend on a prior agreement between client and
 * server. The {@link #prepareVerify} method is how the server indicates
 * to the verify method its understanding of that prior agreement.<p>
 *
 * This proof includes belief in a particular application of
 * the handoff rule...
 * @todo this is one place where we'd decide how often we believed in it.
 */
public class MacProof
	extends Proof {

	ObjectHash subject;	// hash of object
	ObjectHash issuer;	// hash of mac
	ObjectHash compellingArgument; // hash of concatenation of (object, mac)

	private transient boolean preverified = false;
	String errorMessage;

	static final String LABEL = "mac";

	/**
	 * Parse an S-expression into a MacProof object. In a sense, attach
	 * the local verify methods to the remotely-supplied data (proof text).
	 */
	public MacProof(SexpList list)
		throws SexpParseException {
		Assert.assert(list.getType().equals(Proof.LABEL)
					&& ((SexpString) list.elementAt(1)).stringContent()
						.equals(MacProof.LABEL));
		if (list.size()>5) {
			throw new SexpParseException(LABEL+" proof malformed");
		}
		subject = new ObjectHash((SexpList) list.elementAt(2));
		issuer = new ObjectHash((SexpList) list.elementAt(3));
		compellingArgument = new ObjectHash((SexpList) list.elementAt(4));
		srep = list;
	}

	public MacProof(byte[] object, byte[] mac) {
		this(object, KeyTools.hashObject(object),
			mac, KeyTools.hashObject(mac));
	}

	/**
	 * Use this constructor if you already know the MAC's hash
	 * (saves extra hash computations)
	 */
	public MacProof(byte[] object, ObjectHash objectHash,
		byte[] mac, ObjectHash macHash) {
		subject = objectHash;
		issuer = macHash;
		compellingArgument = makeCompellingArgument(object, mac);

		this.subject = objectHash;
		this.issuer = macHash;
		srep = (new SexpList())
			.append(Proof.LABEL)
			.append(MacProof.LABEL)
			.append(subject.getSrep())
			.append(issuer.getSrep())
			.append(compellingArgument.getSrep());
	}

	private ObjectHash makeCompellingArgument(byte[] object, byte[] mac) {
//	Timer t = new Timer();
		MessageDigest md;
		try {
			md = MessageDigest.getInstance("md5");
		} catch (NoSuchAlgorithmException ex) {
			return null;
		}
		md.update(object);
		md.update(mac);
		ObjectHash result = new ObjectHash(new Hash("md5", md.digest()));
//	System.out.println("proof compelling argument digest: "+t);
		return result;
	}

	/**
	 * Tell me the object and secret mac that the hash corresponds to,
	 * so that verify() will work when called in the context of the
	 * rest of the proof. If what you tell me doesn't convince me,
	 * that's fine; we'll just let verify() fail later.
	 */
	public void prepareVerify(byte[] object, byte[] mac) {
		ObjectHash pObjectHash = KeyTools.hashObject(object);
		ObjectHash pMacHash = KeyTools.hashObject(mac);
		ObjectHash pCompellingArgument = makeCompellingArgument(object, mac);
		try {
			if (!pObjectHash.equals(subject)) {
				errorMessage = "prepareVerify(): Object hashes don't match";
				return;
			}
			if (!pMacHash.equals(issuer)) {
				errorMessage = "prepareVerify(): mac hashes don't match";
				return;
			}
			if (!pCompellingArgument.equals(compellingArgument)) {
				errorMessage = "prepareVerify(): compelling argument isn't";
				return;
			}
			errorMessage = null;
			preverified = true;
		} catch (NullPointerException ex) {
			// one of the hashes broke. Fine; we won't be able to verify.
			errorMessage = "prepareVerify(): some hash broke";
		}
	}

	public Subject getSubject() {
		return subject;
	}

	public SDSIPrincipal getIssuer() {
		return issuer;
	}

	/**
	 * This definition of MAC'ing doesn't allow for any tag expression.
	 * One could imagine a form that did.
	 */
	public Tag getTag() {
		return Tag.getTagStar();
	}

	/**
	 * verify that the proof steps are indeed valid, and that they
	 * combine as advertised to show the claimed result
	 */
	protected void directVerify()
		throws InvalidProofException {
		if (preverified) {
			return;
		}
		if (errorMessage != null) {
			throw new InvalidProofException(LABEL+" not verified: "
				+errorMessage);
		} else {
			throw new InvalidProofException(LABEL
				+" not verified: mac verification not prepared by caller.");
		}
	}

	void preorder(List addToMe, int what) {
		// if this node had its own issuer, we'd insert it here.
		switch (what) {
			case Proof.ISSUERS:
				addToMe.add(getIssuer());
				break;
			case Proof.CERTIFICATES:
				break;
			case Proof.PROOFS:
				addToMe.add(this);
				break;
			default:
				throw new RuntimeException("undefined what.");
		}
	}
}
