Manifest.java Source code

Java tutorial

Introduction

Here is the source code for Manifest.java

Source

/*
 * Copyright (c) 2000 David Flanagan.  All rights reserved.
 * This code is from the book Java Examples in a Nutshell, 2nd Edition.
 * It is provided AS-IS, WITHOUT ANY WARRANTY either expressed or implied.
 * You may study, use, and modify it for any non-commercial purpose.
 * You may distribute it non-commercially as long as you retain this notice.
 * For a commercial use license, or to purchase the book (recommended),
 * visit http://www.davidflanagan.com/javaexamples2.
 */

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.DigestInputStream;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.UnrecoverableKeyException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;

/**
 * This program creates a manifest file for the specified files, or verifies an
 * existing manifest file. By default the manifest file is named MANIFEST, but
 * the -m option can be used to override this. The -v option specifies that the
 * manifest should be verified. Verification is also the default option if no
 * files are specified.
 */
public class Manifest {
    public static void main(String[] args) throws Exception {
        // Set the default values of the command-line arguments
        boolean verify = false; // Verify manifest or create one?
        String manifestfile = "MANIFEST"; // Manifest file name
        String digestAlgorithm = "MD5"; // Algorithm for message digests
        String signername = null; // Signer. No sig. by default
        String signatureAlgorithm = "DSA"; // Algorithm for digital sig.
        String password = null; // Private keys are protected
        File keystoreFile = null; // Where are keys stored
        String keystoreType = null; // What kind of keystore
        String keystorePassword = null; // How to access keystore
        List filelist = new ArrayList(); // The files to digest

        // Parse the command-line arguments, overriding the defaults above
        for (int i = 0; i < args.length; i++) {
            if (args[i].equals("-v"))
                verify = true;
            else if (args[i].equals("-m"))
                manifestfile = args[++i];
            else if (args[i].equals("-da") && !verify)
                digestAlgorithm = args[++i];
            else if (args[i].equals("-s") && !verify)
                signername = args[++i];
            else if (args[i].equals("-sa") && !verify)
                signatureAlgorithm = args[++i];
            else if (args[i].equals("-p"))
                password = args[++i];
            else if (args[i].equals("-keystore"))
                keystoreFile = new File(args[++i]);
            else if (args[i].equals("-keystoreType"))
                keystoreType = args[++i];
            else if (args[i].equals("-keystorePassword"))
                keystorePassword = args[++i];

            else if (!verify)
                filelist.add(args[i]);
            else
                throw new IllegalArgumentException(args[i]);
        }

        // If certain arguments weren't supplied, get default values.
        if (keystoreFile == null) {
            File dir = new File(System.getProperty("user.home"));
            keystoreFile = new File(dir, ".keystore");
        }
        if (keystoreType == null)
            keystoreType = KeyStore.getDefaultType();
        if (keystorePassword == null)
            keystorePassword = password;

        if (!verify && signername != null && password == null) {
            System.out.println("Use -p to specify a password.");
            return;
        }

        // Get the keystore we'll use for signing or verifying signatures
        // If no password was provided, then assume we won't be dealing with
        // signatures, and skip the keystore.
        KeyStore keystore = null;
        if (keystorePassword != null) {
            keystore = KeyStore.getInstance(keystoreType);
            InputStream in = new BufferedInputStream(new FileInputStream(keystoreFile));
            keystore.load(in, keystorePassword.toCharArray());
        }

        // If -v was specified or no file were given, verify a manifest
        // Otherwise, create a new manifest for the specified files
        if (verify || (filelist.size() == 0))
            verify(manifestfile, keystore);
        else
            create(manifestfile, digestAlgorithm, signername, signatureAlgorithm, keystore, password, filelist);
    }

    /**
     * This method creates a manifest file with the specified name, for the
     * specified vector of files, using the named message digest algorithm. If
     * signername is non-null, it adds a digital signature to the manifest,
     * using the named signature algorithm. This method can throw a bunch of
     * exceptions.
     */
    public static void create(String manifestfile, String digestAlgorithm, String signername,
            String signatureAlgorithm, KeyStore keystore, String password, List filelist)
            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, KeyStoreException,
            UnrecoverableKeyException, IOException {
        // For computing a signature, we have to process the files in a fixed,
        // repeatable order, so sort them alphabetically.
        Collections.sort(filelist);
        int numfiles = filelist.size();

        Properties manifest = new Properties(), metadata = new Properties();
        MessageDigest md = MessageDigest.getInstance(digestAlgorithm);
        Signature signature = null;
        byte[] digest;

        // If a signer name was specified, then prepare to sign the manifest
        if (signername != null) {
            // Get a Signature object
            signature = Signature.getInstance(signatureAlgorithm);

            // Look up the private key of the signer from the keystore
            PrivateKey key = (PrivateKey) keystore.getKey(signername, password.toCharArray());

            // No prepare to create a signature for the specified signer
            signature.initSign(key);
        }

        // Now, loop through the files, in a well-known alphabetical order
        System.out.print("Computing message digests");
        for (int i = 0; i < numfiles; i++) {
            String filename = (String) filelist.get(i);
            // Compute the digest for each, and skip files that don't exist.
            try {
                digest = getFileDigest(filename, md);
            } catch (IOException e) {
                System.err.println("\nSkipping " + filename + ": " + e);
                continue;
            }
            // If we're computing a signature, use the bytes of the filename
            // and of the digest as part of the data to sign.
            if (signature != null) {
                signature.update(filename.getBytes());
                signature.update(digest);
            }
            // Store the filename and the encoded digest bytes in the manifest
            manifest.put(filename, hexEncode(digest));
            System.out.print('.');
            System.out.flush();
        }

        // If a signer was specified, compute signature for the manifest
        byte[] signaturebytes = null;
        if (signature != null) {
            System.out.print("done\nComputing digital signature...");
            System.out.flush();

            // Compute the digital signature by encrypting a message digest of
            // all the bytes passed to the update() method using the private
            // key of the signer. This is a time consuming operation.
            signaturebytes = signature.sign();
        }

        // Tell the user what comes next
        System.out.print("done\nWriting manifest...");
        System.out.flush();

        // Store some metadata about this manifest, including the name of the
        // message digest algorithm it uses
        metadata.put("__META.DIGESTALGORITHM", digestAlgorithm);
        // If we're signing the manifest, store some more metadata
        if (signername != null) {
            // Store the name of the signer
            metadata.put("__META.SIGNER", signername);
            // Store the name of the algorithm
            metadata.put("__META.SIGNATUREALGORITHM", signatureAlgorithm);
            // And generate the signature, encode it, and store it
            metadata.put("__META.SIGNATURE", hexEncode(signaturebytes));
        }

        // Now, save the manifest data and the metadata to the manifest file
        FileOutputStream f = new FileOutputStream(manifestfile);
        manifest.store(f, "Manifest message digests");
        metadata.store(f, "Manifest metadata");
        System.out.println("done");
    }

    /**
     * This method verifies the digital signature of the named manifest file, if
     * it has one, and if that verification succeeds, it verifies the message
     * digest of each file in filelist that is also named in the manifest. This
     * method can throw a bunch of exceptions
     */
    public static void verify(String manifestfile, KeyStore keystore) throws NoSuchAlgorithmException,
            SignatureException, InvalidKeyException, KeyStoreException, IOException {
        Properties manifest = new Properties();
        manifest.load(new FileInputStream(manifestfile));
        String digestAlgorithm = manifest.getProperty("__META.DIGESTALGORITHM");
        String signername = manifest.getProperty("__META.SIGNER");
        String signatureAlgorithm = manifest.getProperty("__META.SIGNATUREALGORITHM");
        String hexsignature = manifest.getProperty("__META.SIGNATURE");

        // Get a list of filenames in the manifest.
        List files = new ArrayList();
        Enumeration names = manifest.propertyNames();
        while (names.hasMoreElements()) {
            String s = (String) names.nextElement();
            if (!s.startsWith("__META"))
                files.add(s);
        }
        int numfiles = files.size();

        // If we've got a signature but no keystore, warn the user
        if (signername != null && keystore == null)
            System.out.println("Can't verify digital signature without " + "a keystore.");

        // If the manifest contained metadata about a digital signature, then
        // verify that signature first
        if (signername != null && keystore != null) {
            System.out.print("Verifying digital signature...");
            System.out.flush();

            // To verify the signature, we must process the files in exactly
            // the same order we did when we created the signature. We
            // guarantee this order by sorting the filenames.
            Collections.sort(files);

            // Create a Signature object to do signature verification with.
            // Initialize it with the signer's public key from the keystore
            Signature signature = Signature.getInstance(signatureAlgorithm);
            PublicKey publickey = keystore.getCertificate(signername).getPublicKey();
            signature.initVerify(publickey);

            // Now loop through these files in their known sorted order For
            // each one, send the bytes of the filename and of the digest to
            // the signature object for use in computing the signature. It is
            // important that this be done in exactly the same order when
            // verifying the signature as it was done when creating the
            // signature.
            for (int i = 0; i < numfiles; i++) {
                String filename = (String) files.get(i);
                signature.update(filename.getBytes());
                signature.update(hexDecode(manifest.getProperty(filename)));
            }

            // Now decode the signature read from the manifest file and pass
            // it to the verify() method of the signature object. If the
            // signature is not verified, print an error message and exit.
            if (!signature.verify(hexDecode(hexsignature))) {
                System.out.println("\nManifest has an invalid signature");
                System.exit(0);
            }

            // Tell the user we're done with this lengthy computation
            System.out.println("verified.");
        }

        // Tell the user we're starting the next phase of verification
        System.out.print("Verifying file message digests");
        System.out.flush();

        // Get a MessageDigest object to compute digests
        MessageDigest md = MessageDigest.getInstance(digestAlgorithm);
        // Loop through all files
        for (int i = 0; i < numfiles; i++) {
            String filename = (String) files.get(i);
            // Look up the encoded digest from the manifest file
            String hexdigest = manifest.getProperty(filename);
            // Compute the digest for the file.
            byte[] digest;
            try {
                digest = getFileDigest(filename, md);
            } catch (IOException e) {
                System.out.println("\nSkipping " + filename + ": " + e);
                continue;
            }

            // Encode the computed digest and compare it to the encoded digest
            // from the manifest. If they are not equal, print an error
            // message.
            if (!hexdigest.equals(hexEncode(digest)))
                System.out.println("\nFile '" + filename + "' failed verification.");

            // Send one dot of output for each file we process. Since
            // computing message digests takes some time, this lets the user
            // know that the program is functioning and making progress
            System.out.print(".");
            System.out.flush();
        }
        // And tell the user we're done with verification.
        System.out.println("done.");
    }

    /**
     * This convenience method is used by both create() and verify(). It reads
     * the contents of a named file and computes a message digest for it, using
     * the specified MessageDigest object.
     */
    public static byte[] getFileDigest(String filename, MessageDigest md) throws IOException {
        // Make sure there is nothing left behind in the MessageDigest
        md.reset();

        // Create a stream to read from the file and compute the digest
        DigestInputStream in = new DigestInputStream(new FileInputStream(filename), md);

        // Read to the end of the file, discarding everything we read.
        // The DigestInputStream automatically passes all the bytes read to
        // the update() method of the MessageDigest
        while (in.read(buffer) != -1)
            /* do nothing */;

        // Finally, compute and return the digest value.
        return md.digest();
    }

    /** This static buffer is used by getFileDigest() above */
    public static byte[] buffer = new byte[4096];

    /** This array is used to convert from bytes to hexadecimal numbers */
    static final char[] digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

    /**
     * A convenience method to convert an array of bytes to a String. We do this
     * simply by converting each byte to two hexadecimal digits. Something like
     * Base 64 encoding is more compact, but harder to encode.
     */
    public static String hexEncode(byte[] bytes) {
        StringBuffer s = new StringBuffer(bytes.length * 2);
        for (int i = 0; i < bytes.length; i++) {
            byte b = bytes[i];
            s.append(digits[(b & 0xf0) >> 4]);
            s.append(digits[b & 0x0f]);
        }
        return s.toString();
    }

    /**
     * A convenience method to convert in the other direction, from a string of
     * hexadecimal digits to an array of bytes.
     */
    public static byte[] hexDecode(String s) throws IllegalArgumentException {
        try {
            int len = s.length();
            byte[] r = new byte[len / 2];
            for (int i = 0; i < r.length; i++) {
                int digit1 = s.charAt(i * 2), digit2 = s.charAt(i * 2 + 1);
                if ((digit1 >= '0') && (digit1 <= '9'))
                    digit1 -= '0';
                else if ((digit1 >= 'a') && (digit1 <= 'f'))
                    digit1 -= 'a' - 10;
                if ((digit2 >= '0') && (digit2 <= '9'))
                    digit2 -= '0';
                else if ((digit2 >= 'a') && (digit2 <= 'f'))
                    digit2 -= 'a' - 10;
                r[i] = (byte) ((digit1 << 4) + digit2);
            }
            return r;
        } catch (Exception e) {
            throw new IllegalArgumentException("hexDecode(): invalid input");
        }
    }
}