/*******************************************************************************
* Copyright (c) 2013 Lectorius, Inc.
* Authors:
* Vijay Pandurangan (vijayp@mitro.co)
* Evan Jones (ej@mitro.co)
* Adam Hilss (ahilss@mitro.co)
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can contact the authors at inbound@mitro.co.
*******************************************************************************/
package co.mitro.core.crypto;
import java.util.Random;
import java.util.concurrent.PriorityBlockingQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import co.mitro.core.crypto.KeyInterfaces.CryptoError;
import co.mitro.core.crypto.KeyInterfaces.PrivateKeyInterface;
public class PrecomputingKeyczarKeyFactory extends KeyczarKeyFactory {
/** Number of keys to keep cached: This also controls the maximum number of GetPublicKey invites. */
public static final int DEFAULT_QUEUE_SIZE = 200;
/** Keys expire after a random timeout in range [x/2, x). ~1 key/2 minutes. */
private static final int DEFAULT_MAX_KEY_EXPIRY_SEC = DEFAULT_QUEUE_SIZE*4*60;
private static final Logger logger = LoggerFactory.getLogger(PrecomputingKeyczarKeyFactory.class);
private static class Container implements Comparable<Container> {
private static final Random rng = new Random();
public Container(PrivateKeyInterface pki, int maxAgeMs) {
assert (maxAgeMs > 1);
// randomize expiry times to ensure we don't expire everything simultaneously.
expiryTimeMs = System.currentTimeMillis() + maxAgeMs/2 + rng.nextInt(maxAgeMs/2);
this.pki = pki;
}
public final PrivateKeyInterface pki;
private final long expiryTimeMs;
@Override
public int compareTo(Container other) {
// backwards due to priority queue sorting from greatest to least (we want backwards sort)
return (int)(- other.expiryTimeMs + this.expiryTimeMs);
}
@Override
public boolean equals(Object other) {
if (other == null || !(other instanceof Container)) {
return false;
}
return compareTo((Container) other) == 0;
}
@Override
public int hashCode() {
return Long.valueOf(expiryTimeMs).hashCode();
}
}
private final PriorityBlockingQueue<Container> queue = new PriorityBlockingQueue<Container>();
private final int kMaxAgeMs;
private final int kMaxQueueSize;
public PrecomputingKeyczarKeyFactory(int keysToPrecompute, int maxKeyAgeSeconds) {
kMaxQueueSize = keysToPrecompute;
kMaxAgeMs = maxKeyAgeSeconds * 1000;
logger.info("Starting keyczar factory background task");
Thread backgroundTask = new Thread(new QueueFiller());
// mark as daemon so JVM exits if this thread is still running
backgroundTask.setDaemon(true);
backgroundTask.start();
}
public PrecomputingKeyczarKeyFactory() {
this(DEFAULT_QUEUE_SIZE, DEFAULT_MAX_KEY_EXPIRY_SEC);
}
@Override
public PrivateKeyInterface generate() throws CryptoError {
for (;;) {
try {
return queue.take().pki;
} catch (InterruptedException e) {
logger.error("This should not have happened", e);
}
}
}
private class QueueFiller implements Runnable {
public void run() {
logger.debug("Started filling queue");
for (;;) {
// get rid of expired keys
for (;;) {
Container frontKey = queue.peek();
if (frontKey == null || frontKey.expiryTimeMs > System.currentTimeMillis()) {
break;
}
// use poll, not take so we don't block (in case something has emptied
//queue between this and previous statement.)
if (null != (frontKey = queue.poll())) {
// to avoid race condition
logger.info("expiring precomputed key with timestamp {}", frontKey.expiryTimeMs);
}
}
// make new keys
while (queue.size() < kMaxQueueSize) {
try {
logger.info("precomputing key. ({}/{})", queue.size(), kMaxQueueSize);
queue.put(new Container(PrecomputingKeyczarKeyFactory.super.generate(), kMaxAgeMs));
} catch (CryptoError e) {
logger.error("unexpected crypto error when generating keys; ignored. Hope this doesn't break something", e);
}
}
// wait, then try this again.
try {
Thread.sleep(250);
} catch (InterruptedException e) {
logger.error("unexpected exception", e);
}
}
}
}
}