uk.ac.ox.webauth.PrivateKeyManager.java Source code

Java tutorial

Introduction

Here is the source code for uk.ac.ox.webauth.PrivateKeyManager.java

Source

/*
 * Webauth Java - Java implementation of the University of Stanford WebAuth
 * protocol.
 *
 * Copyright (C) 2006 University of Oxford
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package uk.ac.ox.webauth;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import javax.servlet.ServletException;
import org.apache.commons.codec.binary.Hex;

/**
 * Key manager for the WAS private session keys. It can more or less safely
 * share it concurrently with mod_webauth.
 *
 * <p/>$HeadURL$
 * <br/>$LastChangedRevision$
 * <br/>$LastChangedDate$
 * <br/>$LastChangedBy$
 * @author     Mats Henrikson
 * @version    $LastChangedRevision$
 */
public class PrivateKeyManager {

    /** Where to log to. */
    private final LogWrapper logger;
    /** The keyring. */
    private File keyring;
    /** The time the keyring was last modified. */
    private long lastModified;
    /** When we last checked the keyring for changes. */
    private long lastChecked;
    /** Timestamp when we last checked if a key should be removed or added. */
    private long updateChecked;
    /** A SortedMap holding all the keys, sorted by valid-after time. */
    private List<WebauthKey> keys;
    /** The v key of the keyring. Not sure what it does but it might come in useful. */
    private int v;
    /** Should we create new keys when the old ones become too old? */
    private boolean createKeys;
    /** Should we delete keys from the keyring when they become too old? */
    private boolean removeKeys;

    /** Minimum interval in milliseconds between checking the keyring for updates by another process. */
    private static final long KEYRING_UPDATE_CHECK_INTERVAL = 2000; // 2 seconds
    /** Minimum interval in milliseconds between checking if a the keyring should be updated by this manager. */
    private static final long KEYRING_UPDATE_INTERVAL = 60000; // 1 minute
    /** How old the va value of the freshest key must be before a new key is created, in seconds. */
    private static final int KEY_NEW_AGE = 2592000; // 30 days
    /** How old the va value of a key must be before it is removed from the keyring, in seconds. */
    private static final int KEY_REMOVE_AGE = 7776000; // 90 days
    /** Randomiser to create keys with. */
    private static final SecureRandom RAND = new SecureRandom();

    /** Prints out a keyring given in args[0]. */
    public static void main(String[] args) throws ServletException {
        PrivateKeyManager pkm = new PrivateKeyManager(args[0], false, false, new LogWrapper());
        System.out.println(pkm.toString());
    }

    /**
     * Initialise the key manager.
     * @param   keyring         The location of the Webauth keyring.
     * @param   createKeys      Should the manager create new keys?
     * @param   removeKeys      Should the manager remove old keys?
     * @param   logger          The log object, for logging to.
     * @throws  ServletException    if it wasn't possible to load the keyring.
     */
    public PrivateKeyManager(String keyring, boolean createKeys, boolean removeKeys, LogWrapper logger)
            throws ServletException {
        this.logger = logger;
        this.keyring = new File(keyring);
        this.createKeys = createKeys;
        this.removeKeys = removeKeys;
        try {
            loadAndParse(this.keyring);
        } catch (IOException ioe) {
            throw new ServletException(ioe);
        }
        if (createKeys && !this.keyring.canWrite()) {
            throw new ServletException("Cannot write to keyring file '" + this.keyring.getPath() + "'.");
        }
    }

    /**
     * Return a list of keys that may be suitable to decode the token, most
     * suitable first.
     * @param   keyHint The token key hint.
     * @return  A List of suitable keys, most suitable first. An empty list if
     *          there are no suitable keys.
     * @throws  ServletException    if there is a problem checking the keyring.
     */
    public List<WebauthKey> suitableKeys(int keyHint) throws ServletException {
        long now = System.currentTimeMillis();

        // check if it's time to check the keyring for changes again
        if (lastChecked + KEYRING_UPDATE_CHECK_INTERVAL < now) {
            lastChecked = now;
            long mt = keyring.lastModified();
            if (lastModified == mt) {
                if (logger.debug()) {
                    logger.debug("Keyring " + keyring.getPath()
                            + " appears not to have been modified, not reloading it.");
                }
            } else {
                lastModified = mt;
                try {
                    loadAndParse(keyring);
                } catch (IOException ioe) {
                    throw new ServletException(ioe);
                }
            }
        } else {
            if (logger.debug()) {
                logger.debug("Not checking keyring " + keyring.getPath() + ", last checked " + new Date(lastChecked)
                        + ".");
            }
        }

        // check if a key should be removed or added
        if (updateChecked + KEYRING_UPDATE_INTERVAL < now) {
            updateChecked = now;
            synchronized (keys) {
                boolean modified = false;
                if (createKeys) {
                    if (keys.size() < 1 || (keys.get(0).va() + KEY_NEW_AGE) * 1000L < now) {
                        byte[] keyData = new byte[16];
                        RAND.nextBytes(keyData);
                        int timestamp = (int) (now / 1000);
                        keys.add(0, new WebauthKey(keys.size(), timestamp, timestamp, 1,
                                new String(Hex.encodeHex(keyData))));
                        modified = true;
                        if (logger.debug()) {
                            logger.debug(
                                    "Added a new key to keyring: " + keyring.getPath() + ",\n" + this.toString());
                        }
                    }

                }
                if (removeKeys) {
                    for (Iterator<WebauthKey> i = keys.iterator(); i.hasNext();) {
                        WebauthKey key = i.next();
                        if ((key.va() + KEY_REMOVE_AGE) * 1000L < now) {
                            i.remove();
                            modified = true;
                            if (logger.debug()) {
                                logger.debug("Removed key " + key.kn() + " from keyring " + keyring.getPath()
                                        + " as it had passed its max age of: "
                                        + new Date((key.va() + KEY_REMOVE_AGE) * 1000L) + ".");
                            }
                        }
                    }
                }
                if (modified) {
                    try {
                        saveKeys(keyring);
                        loadAndParse(keyring);
                    } catch (IOException ioe) {
                        throw new ServletException(ioe);
                    }
                } else {
                    if (logger.debug()) {
                        logger.debug("Did not update keyring " + keyring.getPath() + ".");
                    }
                }
            }
        } else {
            if (logger.debug()) {
                logger.debug("Not considering adding or removing keys from keyring " + keyring.getPath()
                        + ", last checked: " + new Date(updateChecked) + ".");
            }
        }

        // make a backup ref to the current keys in case a new keyring is loaded
        List<WebauthKey> keys = this.keys;
        List<WebauthKey> suitable = new ArrayList<WebauthKey>();
        for (WebauthKey key : keys) {
            // if the key hint is larger (i.e. more recent) than the valid-after of the key then it is suitable
            if (keyHint >= key.va() && key.va() * 1000L <= now) {
                suitable.add(key);
            }
        }
        if (logger.debug()) {
            logger.debug("Found " + suitable.size() + " suitable keys for keyHint " + keyHint + ".");
        }
        return suitable;
    }

    /**
     * Saves all the keys we know about to a keyring.
     * @param   keyring The path to save the keys to.
     */
    private synchronized void saveKeys(File keyring) throws IOException {
        Writer out = null;
        try {
            out = new BufferedWriter(new FileWriter(keyring));
            out.append("v=").append(Integer.toString(v)).append(";");
            out.append("n=").append(Integer.toString(keys.size())).append(";");
            List<WebauthKey> sortedKeys = new ArrayList<WebauthKey>(keys);
            Collections.sort(sortedKeys, new Comparator<WebauthKey>() {
                public int compare(WebauthKey key1, WebauthKey key2) {
                    return key1.kn() - key2.kn();
                }
            });
            for (int i = 0; i < sortedKeys.size(); i++) {
                WebauthKey key = sortedKeys.get(i);
                out.append("ct").append(Integer.toString(i)).append("=").append(Integer.toString(key.ct()))
                        .append(";");
                out.append("va").append(Integer.toString(i)).append("=").append(Integer.toString(key.va()))
                        .append(";");
                out.append("kt").append(Integer.toString(i)).append("=").append(Integer.toString(key.kt()))
                        .append(";");
                out.append("kd").append(Integer.toString(i)).append("=").append(key.kd()).append(";");
            }
        } catch (IOException ioe) {
            logger.error(ioe.getMessage(), ioe);
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException ioe) {
                    logger.error(ioe.getMessage(), ioe);
                }
            }
        }
        if (logger.debug()) {
            logger.debug("Saved " + keys.size() + " keys to the keyring: " + keyring.getPath() + ".");
        }
    }

    /**
     * Return the most suitable key, usually to use for encryption.
     * @return  the most suitable key to use at the moment.
     * @throws  ServletException    if there is no suitable key.
     */
    public WebauthKey mostSuitable() throws ServletException {
        List<WebauthKey> suitable = suitableKeys((int) (System.currentTimeMillis() / 1000L));
        if (suitable.size() == 0) {
            throw new ServletException("There are no suitable keys in the keyring.");
        }
        WebauthKey key = suitable.get(0);
        if (logger.debug()) {
            logger.debug("The single most suitable key at this point in time is key " + key.kn() + ".");
        }
        return key;
    }

    /**
     * Loads and parses a keyring.
     * @param   keyring The keyring file to load.
     * @throws  IOException if it can't read the keyring.
     */
    private synchronized void loadAndParse(File keyring) throws IOException, ServletException {
        BufferedReader in = new BufferedReader(new FileReader(keyring));
        String line = in.readLine();
        String[] keyData = (line == null) ? new String[0] : line.split("(;|=)");
        in.close();
        int n = 0;
        int va = -1;
        int ct = -1;
        int kt = -1;
        int kn = -1;
        String kd = null;
        List<WebauthKey> newKeys = new ArrayList<WebauthKey>();
        for (int i = 0; i < keyData.length; i++) {
            switch (keyData[i].charAt(0)) {
            case 'v': // v or va
                if (keyData[i].length() == 1) {
                    v = parseInt(keyData[++i]);
                } else {
                    va = parseInt(keyData[++i]);
                }
                break;

            case 'n': // n = number of keys
                n = parseInt(keyData[++i]);
                break;

            case 'c': // ct = created time
                // also initialise the key number
                kn = parseInt(keyData[i].substring(2));
                ct = parseInt(keyData[++i]);
                break;

            case 'k': // kt or kd
                if (keyData[i].charAt(1) == 't') {
                    kt = parseInt(keyData[++i]);
                } else {
                    kd = keyData[++i];
                }
                break;

            default: // anything else is an error
                throw new IOException("Received unknown token '" + keyData[i] + "' in keyring data.");
            }
            if (va != -1 && ct != -1 && kt != -1 && kd != null) {
                newKeys.add(0, new WebauthKey(kn, ct, va, kt, kd));
                va = ct = -1;
                kt = kn = -1;
                kd = null;
            }
        }
        if (newKeys.size() != n) {
            throw new IOException("Read " + newKeys.size() + " keys from the keyring, expected " + n);
        }
        if (logger.debug()) {
            logger.debug("Loaded " + n + " keys from the keyring.");
        }
        Collections.sort(newKeys);
        Collections.reverse(newKeys);
        keys = newKeys;
    }

    /** Convenience method. */
    private static int parseInt(String data) throws IOException {
        try {
            return Integer.parseInt(data);
        } catch (NumberFormatException nfe) {
            IOException ioe = new IOException("Could not parse '" + data + "' into an int.");
            ioe.initCause(nfe);
            throw ioe;
        }
    }

    /** Convenience method. */
    private static long parseLong(String data) throws IOException {
        try {
            return Long.parseLong(data);
        } catch (NumberFormatException nfe) {
            IOException ioe = new IOException("Could not parse '" + data + "' into a long.");
            ioe.initCause(nfe);
            throw ioe;
        }
    }

    /** Print out the keys this key manager knows about. */
    public String toString() {
        List<WebauthKey> reversedKeys = new ArrayList<WebauthKey>(keys);
        Collections.reverse(reversedKeys);
        StringBuffer sb = new StringBuffer();
        for (WebauthKey key : reversedKeys) {
            sb.append("Key:         ").append(key.kn()).append("\n");
            sb.append("Key type:    ").append(key.kt()).append("\n");
            sb.append("Created:     ").append(new Date(key.ct() * 1000L)).append("\n");
            sb.append("Valid after: ").append(new Date(key.va() * 1000L)).append("\n");
            sb.append("-----------------------------------------\n");
        }
        return sb.toString();
    }
}