// ResInfoExtension.java
// $Id: ResInfoExtension.java,v 1.1 1997/04/15 20:39:13 yhchu Exp $
// (c) COPYRIGHT MIT and INRIA, 1997.
// Please first read the full copyright statement in file COPYRIGHT.html

// The source code is jointly developed by W3C and some of its members
// participating in the Digital Signature Initiative.  The authors are:
//    Mark Champine, HP, <champine@apollo.hp.com>
//    Yang-hua Chu, MIT/W3C, <yhchu@w3.org>
//    Vasanthan Dasan, Sun, <vasanthan.dasan@central.sun.com>
//    Peter Lipp, University of Technology, Graz <plipp@iaik.tu-graz.ac.at>
//    Andreas Sterbenz, U. of Technology, Graz <sterbenz@iaik.tu-graz.ac.at>


package w3c.www.dsig;

import java.io.* ;
import java.util.* ;
import java.security.*;
import java.net.URL;
import w3c.tools.codec.*;
import w3c.tools.sexpr.*;
import w3c.tools.sorter.*;

/**
 * ResInfo as defined by W3C Digital Signature Initative.
 * The ResInfo (resource information) is a PICS-1.1 extension
 * that contains zero or more cryptographic hashes of the document
 * the label references. Hashes are stored in the resinfo as name
 * value pairs where name is a URL identifying the message digest
 * algorithm and the value is the base64 encoded hash as defined
 * in the respective hash algorithm specification. Optionally,
 * each hash can contain the date it was created.
 * <P>
 * This class implements this set of message digests and also
 * supports calculating and verifying hashes.
 * <P>
 * Example:
 * <PRE>
 *  extension
 *    ( optional "http://www.w3.org/TR/1998/REC-DSig-label/resinfo-1_0"
 *       ( "http://www.w3.org/TR/1998/REC-DSig-label/SHA1" "base64-hash" )
 *       ( "http://www.w3.org/TR/1998/REC-DSig-label/MD5" "base64-hash"
 *         "1997.02.05T08:15-0500" ) )
 * </PRE>
 *
 * @author Yang-hua Chu
 * @author Andreas Sterbenz
 * @version 1.0beta (last modified 29-October-1997)
 */
public class ResInfoExtension {

  // class internal variables
  private Hashtable mds;
  private static final boolean DEBUG = false;
  private final int BUFFER_SIZE = 8192;

  /**
   * The name of the extension identifier
   * (<TT>http://www.w3.org/TR/1998/REC-DSig-label/resinfo-1_0</TT>).
   */
  public static final String
  EXTENSION_ID = "http://www.w3.org/TR/1998/REC-DSig-label/resinfo-1_0";


  /**
   * Create an empty resource information
   */
  public ResInfoExtension()  {
    // YC-D: super();
    this.mds = new Hashtable();
    // YC-D: setURL(EXTENSION_ID);
  }

  private InputStream openStream(DSigLabelInterface label)
    throws IOException, DSigException
  {
    String urlString = label.getFor();
    if( urlString == null ) {
      throw new DSigException("No 'for' option specified in label, cannot calculate hash!");
    }
    URL url = new URL(urlString);
    InputStream ins = url.openStream();
    return ins;
  }

  /**
   * compute a message digest (internal use only)
   */
  private String calcDigest(String alg, InputStream ins)
    throws NoSuchAlgorithmException, IOException
  {
    MessageDigest md = HashRegistry.getInstance(alg);
  	byte buffer[] = new byte[BUFFER_SIZE];
  	int n=-1;

  	while( (n = ins.read(buffer, 0, BUFFER_SIZE)) > 0 )
  	{
      md.update(buffer, 0, n);
  	}
  	String bindigest = new String(md.digest());
    Base64Encoder base64 = new Base64Encoder(bindigest);
    return base64.processString();
  }


  /**
   * Add the pre-computed message digest <CODE>val</CODE> for the
   * algorithm <CODE>alg</CODE>. The digest <CODE>val</CODE> needs
   * to be in the correct base 64 encoded format.
   * If date is not null it is added as the digest's date.
   */
  public void addDigest(String alg, String val, ISODate date) {
    Vector digest = new Vector();
    digest.addElement(val);
    if (date != null)
      digest.addElement(date);
    this.mds.put(alg, digest);
  }

  /**
   * Calculate the message digest of the data from the InputStream ins
   * and add it to the resinfo.
   * If date is not null it is added as the digest's date.
   * @exception NoSuchAlgorithmException if the corresponding message digest
   * algorithm cannot be found
   * @exception IOException if the input stream cannot be properly read.
   */
  public void addDigest(String alg, InputStream ins, ISODate date)
    throws NoSuchAlgorithmException, IOException
  {
    addDigest(alg, calcDigest(alg, ins), date);
  }

  /**
   * Calculate the message digest of the data referenced by the URL 
   * specified in the 'for' option of the label.
   * If date is not null it is added as the digest's date.
   * @exception NoSuchAlgorithmException if the corresponding message digest
   * algorithm cannot be found
   * @exception IOException if it cannot open a URL stream.
   * @exception DSigException if there is no 'for' option in the label 
   * (i.e. the label does not specify which URL to perform digest).
   */
  public void addDigest(String alg, DSigLabelInterface label, ISODate date)
    throws NoSuchAlgorithmException, IOException, DSigException
  {
    addDigest(alg, openStream(label), date);
  }

  /**
   * Perform verification using the default policy as defined 
   * by the user.
   * However, this is not yet implemented and the method 
   * is equivalent to verify(label).
   */
  public Trivalue verify(DSigLabelInterface label)
  {
    return verifyAll(label);
  }

  /**
   * Perform verification using the default policy as defined 
   * by the user.
   * However, this is not yet implemented and the method 
   * is equivalent to verifyAll(ins).
   */
  public Trivalue verify(InputStream ins)
  {
    return verifyAll(ins);
  }

  /**
   * Verify if the hash of the document specified in the 'for'
   * option of the label matches the hash stored in this resinfo.
   */
  public Trivalue verify(String alg, DSigLabelInterface label)
  {
    try {
      return verify(alg, openStream(label));
    } catch( Exception e ) {
      return Trivalue.newUnknown();
    }
  }

  /**
   * Verify if the hash of the data in the InputStream ins equals
   * the hash stored in this resinfo.
   */
  public Trivalue verify(String alg, InputStream ins)
  {
    String value = getDigestValue(alg);
    if( value == null ) return Trivalue.newUnknown();
    try {
      return new Trivalue(value.equals(calcDigest(alg, ins)));
    } catch( NoSuchAlgorithmException e ) {}
      catch( IOException e) {}
    return Trivalue.newUnknown();
  }

  /**
   * Verify if all hashes in the ResInfo extension match the
   * document specified in the 'for' option of the label.
   */
  public Trivalue verifyAll(DSigLabelInterface label)
  {
    try {
      return verifyAll(openStream(label));
    } catch( Exception e ) {
      return Trivalue.newUnknown();
    }
  }

  /**
   * Verify if all hashes in the ResInfo extension match the
   * data from the InputStream. Hashing is done in parallel in a single
   * scan of the InputStream so there is no need to reset or reopen
   * the inputstream.
   */
  public Trivalue verifyAll(InputStream ins)
  {
    // init
    int hashnr = mds.size();
    if( hashnr == 0 ) return Trivalue.newUnknown();
    MessageDigest[] hashalgs = new MessageDigest[hashnr];
    String[] hashvalues = new String[hashnr];
    String[] algnames = new String[hashnr];
    int i=0;
    for (Enumeration e = this.digests() ; e.hasMoreElements() ;) {
      String alg = (String)e.nextElement();
      algnames[i] = alg;
      hashvalues[i] = getDigestValue(alg);
      try {
        hashalgs[i] = HashRegistry.getInstance(alg);
      } catch( NoSuchAlgorithmException exc ) {
        hashalgs[i] = null;
      }
      i++;
    }
    // do the hashing
  	byte buffer[] = new byte[BUFFER_SIZE];
  	int n=-1;

    try {
      while( (n = ins.read(buffer, 0, BUFFER_SIZE)) > 0 )
      {
  	    for( i=0; i<hashnr; i++ ) {
          if( hashalgs[i] != null ) hashalgs[i].update(buffer, 0, n);
        }
  	  }
  	} catch( IOException e ) {
  	  return Trivalue.newUnknown();
  	}
    // compute the result
    Trivalue result = Trivalue.newTrue();
  	for( i=0; i<hashnr; i++ ) {
  	  Trivalue res1;
  	  if( hashalgs[i] == null ) {
  	    res1 = Trivalue.newUnknown();
  	  } else {
        String bindigest = new String(hashalgs[i].digest());
        Base64Encoder base64 = new Base64Encoder(bindigest);
        res1 = new Trivalue(hashvalues[i].equals(base64.processString()));
      }
      if(DEBUG) System.out.println("Verifying " + algnames[i] + ": " + res1);
      result = result.and(res1);
    }
    return result;
  }

  /**
   * Remove the message digest for the algorithm alg. Return true
   * if successful, false otherwise (no message digest for
   * algorithm alg present).
   */
  public boolean removeDigest(String alg)
  {
    return this.mds.remove(alg) != null;
  }
  
  /**
   * Check if a message digest for the given algorithm exists
   * in this resinfo.
   */
  public boolean containsDigest(String alg)
  {
    return this.mds.containsKey(alg); 
  }

  /**
   * Return the number of hashes in this ResInfo.
   */
  public int size()
  {
    return this.mds.size();
  }

  /**
   * Enumerate the digest URLs.
   */
  public Enumeration digests()
  {
    return this.mds.keys();
  }

  /**
   * Get the value of a digest function. Return null if not found.
   * Applications should not need to use this, it is intended for
   * internal use.
   */
  public String getDigestValue(String alg)
  {
    if( this.mds.get(alg) == null ) return null;
    return (String)((Vector)this.mds.get(alg)).elementAt(0);
  }


  /**
   * Get the ISODate of a digest function. Return null if the
   * algorithm is not found or no date is present.
   */
  public ISODate getDigestDate(String alg)
  {
    if( this.mds.get(alg) == null ) return null;
    Vector digest = (Vector) this.mds.get(alg);
    if (digest.size() == 1) {
      return null;
    } else {
      return (ISODate) digest.elementAt(1);
    }
  }


  /**
   * Parse a Vector to a ResInfo extension.
   */
  public void parse(Vector v)
  {
    for (int i=0; i<v.size(); i++) {
      String name = (String) ((Vector)v.elementAt(i)).elementAt(0);
      String val = (String) ((Vector) v.elementAt(i)).elementAt(1);
      if (((Vector)v.elementAt(i)).size() == 2) {
        addDigest(name, val, null);
      } else {
        addDigest(name, val, (new ISODate((String)
          ((Vector)v.elementAt(i)).elementAt(2))));
      }
    }
  }


  /**
   * Return a normalized, Vector form of ResInfo Extension.
   */
  public Vector toVector()
  {
    Vector v1 = new Vector();
    Vector sorted = Sorter.sortObjectEnumeration(this.digests());
    for (int i=0; i<sorted.size(); i++) {
      Vector v2 = new Vector();
      v1.addElement(v2);
      String key = (String)sorted.elementAt(i);
      v2.addElement(key);
      v2.addElement(getDigestValue(key));
      ISODate date = getDigestDate(key);
      if( date != null ) {
        v2.addElement(date);
      }
    }
    return v1;
  }


  /**
   * Return an S-Expression string representation of this
   * ResInfo extension.
   */
  public String toString() {
    String s = "";
    Vector v = toVector();
    for (int i=0; i< v.size(); i++)
      s += DSigUtil.toString(v.elementAt(i)) + " ";
    return s;
  }
  

  /**
   * parse a UserOptionValue to a ResInfo extension
   */
  // YC-D
//   void parse(UserOptionValue optionVal)
//     throws SigLabelException
//   {
//     if (optionVal.isMandatory()) {
//       setMandatory();
//     } else {
//       setOptional();
//     }
//     Object v[] = optionVal.getData();
//     for (int i=0; i<v.length; i++) {
//       String name = (String) ((Vector)v[i]).elementAt(0);
//       String val = (String) ((Vector) v[i]).elementAt(1);
//       if (((Vector)v[i]).size() == 2) {
//         addDigest(name, val, null);
//       } else {
//         addDigest(name, val, (new ISODate((String)
//           ((Vector)v[i]).elementAt(2))));
//       }
//     }
//   }


//   /**
//    * toUserOptionValue, normalized
//    */
//   UserOptionValue toUserOptionValue()
//   {
//     Vector v1[] = new Vector[mds.size()];
//     Vector sorted = Sorter.sortObjectEnumeration(this.digests());
//     for (int i=0; i<sorted.size(); i++) {
//       Vector v2 = new Vector();
//       v1[i] = v2;
//       String key = (String)sorted.elementAt(i);
//       v2.addElement(key);
//       v2.addElement(getDigestValue(key));
//       ISODate date = getDigestDate(key);
//       if( date != null ) {
//         v2.addElement(date);
//       }
//     }
//     setData(v1);
//     return (UserOptionValue)this;
//   }


//   /**
//    * Return an S-Expression string representation of this
//    * ResInfo extension.
//    */
//   public String toString() {
//     toUserOptionValue();
//     return super.toString();
//   }

}

