ProductDigest.java
/*
* ProductDigest
*/
package gov.usgs.earthquake.product;
import gov.usgs.earthquake.product.io.ObjectProductSource;
import gov.usgs.earthquake.product.io.ProductHandler;
import gov.usgs.earthquake.util.NullOutputStream;
import gov.usgs.util.CryptoUtils;
import gov.usgs.util.StreamUtils;
import gov.usgs.util.XmlUtils;
import gov.usgs.util.CryptoUtils.Version;
import java.io.File;
import java.net.URI;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.DigestOutputStream;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Date;
import java.util.Iterator;
import java.util.logging.Logger;
/**
* Used to generate product digests.
*
* All product attributes and content are used when generating a digest, except
* any existing signature, since the digest is used to generate or verify
* signatures.
*
* Calls to ProductOutput methods on this class must occur in identical order to
* generate consistent signatures. Therefore it is almost required to use the
* ObjectProductInput, which fulfills this requirement.
*/
public class ProductDigest implements ProductHandler {
/** Logging object. */
private static final Logger LOGGER = Logger.getLogger(ProductDigest.class
.getName());
/** Character set used when computing digests. */
public static final Charset CHARSET = StandardCharsets.UTF_8;
/** Algorithm used when generating product digest. */
public static final String MESSAGE_DIGEST_ALGORITHM = "SHA1";
/** v2 digest algorithm */
public static final String MESSAGE_DIGEST_V2_ALGORITHM = "SHA-256";
/** The stream used to compute the product digest. */
private DigestOutputStream digestStream;
/** The computed digest. */
private byte[] digest = null;
/** The signature version. */
private Version version = null;
/**
* Construct a new ProductDigest.
* @param version signature version
* @throws NoSuchAlgorithmException if not SHA1 or SHA-256
*/
protected ProductDigest(final Version version) throws NoSuchAlgorithmException {
final String algorithm = version == Version.SIGNATURE_V2
? MESSAGE_DIGEST_V2_ALGORITHM
: MESSAGE_DIGEST_ALGORITHM;
LOGGER.fine("Using digest version " + version.toString()
+ ", algorithm=" + algorithm);
MessageDigest digest = MessageDigest.getInstance(algorithm);
this.digestStream = new DigestOutputStream(new NullOutputStream(), digest);
this.version = version;
}
/**
* A convenience method that generates a product digest.
*
* @param product
* the product to digest
* @return the computed digest.
* @throws Exception
* if errors occur while digesting product.
*/
public static byte[] digestProduct(final Product product) throws Exception {
return digestProduct(product, Version.SIGNATURE_V1);
}
/**
*
* @param product A product
* @param version What version of product digest
* @return A byte array of the product digest
* @throws Exception if error occurs
*/
public static byte[] digestProduct(final Product product, final Version version)
throws Exception {
Date start = new Date();
ProductDigest productDigest = new ProductDigest(version);
// ObjectProductInput generates ProductOutput calls in a reliable order.
new ObjectProductSource(product).streamTo(productDigest);
Date end = new Date();
byte[] digest = productDigest.getDigest();
LOGGER.fine("Digest='" + Base64.getEncoder().encodeToString(digest)
+ "' , " + (end.getTime() - start.getTime()) + "ms");
return digest;
}
/**
* @return the computed digest, or null if not finished yet.
*/
public byte[] getDigest() {
return digest;
}
/**
* Digest the id, update time, status, and URL.
*/
public void onBeginProduct(ProductId id, String status, URL trackerURL)
throws Exception {
digestStream.write(id.toString().getBytes(CHARSET));
digestStream.write(XmlUtils.formatDate(id.getUpdateTime()).getBytes(
CHARSET));
digestStream.write(status.getBytes(CHARSET));
if (this.version != Version.SIGNATURE_V2 && trackerURL != null) {
digestStream.write(trackerURL.toString().getBytes(CHARSET));
}
}
/**
* Digest the path, content attributes, and content bytes.
*/
public void onContent(ProductId id, String path, Content content)
throws Exception {
digestStream.write(path.getBytes(CHARSET));
digestStream.write(content.getContentType().getBytes(CHARSET));
digestStream.write(XmlUtils.formatDate(content.getLastModified())
.getBytes(CHARSET));
digestStream.write(content.getLength().toString().getBytes(CHARSET));
if (this.version == Version.SIGNATURE_V2) {
digestStream.write(content.getSha256().getBytes(CHARSET));
} else {
StreamUtils.transferStream(content.getInputStream(),
new StreamUtils.UnclosableOutputStream(digestStream));
}
}
/**
* Finish computing digest.
*/
public void onEndProduct(ProductId id) throws Exception {
// finish computing message digest.
digestStream.flush();
digest = digestStream.getMessageDigest().digest();
}
/**
* Digest the link relation and href.
*/
public void onLink(ProductId id, String relation, URI href)
throws Exception {
digestStream.write(relation.getBytes(CHARSET));
digestStream.write(href.toString().getBytes(CHARSET));
}
/**
* Digest the property name and value.
*/
public void onProperty(ProductId id, String name, String value)
throws Exception {
digestStream.write(name.getBytes(CHARSET));
digestStream.write(value.getBytes(CHARSET));
}
/**
* Don't digest signature version.
*/
@Override
public void onSignatureVersion(ProductId id, Version version) throws Exception {
// generating signature, ignore
}
/**
* Don't digest the signature.
*/
@Override
public void onSignature(ProductId id, String signature) throws Exception {
// generating signature, ignore
}
/**
* Free any resources associated with this handler.
*/
@Override
public void close() {
StreamUtils.closeStream(digestStream);
}
/**
* CLI access into ProductDigest
* @param args CLI Args
* @throws Exception if error occurs
*/
public static void main(final String[] args) throws Exception {
if (args.length == 0) {
System.err.println("Usage: ProductDigest FILE [FILE ...]");
System.err
.println("where FILE is a file or directory to include in digest");
System.exit(1);
}
Product product = new Product(new ProductId("test", "test", "test"));
try {
product.setTrackerURL(new URL("http://localhost/tracker"));
} catch (Exception e) {
// ignore
}
// treat all arguments as files or directories to be added as content
for (String arg : args) {
File file = new File(arg);
if (!file.exists()) {
System.err.println(file.getCanonicalPath() + " does not exist");
System.exit(1);
}
if (file.isDirectory()) {
product.getContents().putAll(
FileContent.getDirectoryContents(file));
} else {
product.getContents()
.put(file.getName(), new FileContent(file));
}
}
long totalBytes = 0L;
Iterator<String> iter = product.getContents().keySet().iterator();
while (iter.hasNext()) {
totalBytes += product.getContents().get(iter.next()).getLength();
}
KeyPair keyPair = CryptoUtils.generateDSAKeyPair(CryptoUtils.DSA_1024);
Date start = new Date();
product.sign(keyPair.getPrivate());
Date end = new Date();
long elapsed = (end.getTime() - start.getTime());
System.err.println("Digested " + totalBytes + " bytes of content in "
+ elapsed + "ms");
System.err.println("Average rate = " + (totalBytes / (elapsed / 1000.0))
+ " bytes/second");
}
}