package gnu.crypto.prng; import java.util.Collections; import java.util.Map; import java.util.concurrent.LinkedBlockingQueue; import net.i2p.I2PAppContext; import net.i2p.util.I2PThread; import net.i2p.util.Log; /** * fortuna instance that tries to avoid blocking if at all possible by using separate * filled buffer segments rather than one buffer (and blocking when that buffer's data * has been eaten) * * Note that this class is not fully Thread safe! * The following methods must be synchronized externally, they are not * synced here or in super(): * addRandomByte(), addRandomBytes(), nextByte(), nextBytes(), seed() * */ public class AsyncFortunaStandalone extends FortunaStandalone implements Runnable { /** * This is set to 2 to minimize memory usage for standalone apps. * The router must override this via the prng.buffers property in the router context. */ private static final int DEFAULT_BUFFERS = 2; private static final int DEFAULT_BUFSIZE = 256*1024; private final int _bufferCount; private final int _bufferSize; /** the lock */ private final Object asyncBuffers = new Object(); private final I2PAppContext _context; private final Log _log; private volatile boolean _isRunning; private Thread _refillThread; private final LinkedBlockingQueue<AsyncBuffer> _fullBuffers; private final LinkedBlockingQueue<AsyncBuffer> _emptyBuffers; private AsyncBuffer _currentBuffer; public AsyncFortunaStandalone(I2PAppContext context) { super(); _bufferCount = Math.max(context.getProperty("prng.buffers", DEFAULT_BUFFERS), 2); _bufferSize = Math.max(context.getProperty("prng.bufferSize", DEFAULT_BUFSIZE), 16*1024); _emptyBuffers = new LinkedBlockingQueue<AsyncBuffer>(_bufferCount); _fullBuffers = new LinkedBlockingQueue<AsyncBuffer>(_bufferCount); _context = context; context.statManager().createRequiredRateStat("prng.bufferWaitTime", "Delay for random number buffer (ms)", "Encryption", new long[] { 60*1000, 10*60*1000, 60*60*1000 } ); context.statManager().createRequiredRateStat("prng.bufferFillTime", "Time to fill random number buffer (ms)", "Encryption", new long[] { 60*1000, 10*60*1000, 60*60*1000 } ); _log = context.logManager().getLog(AsyncFortunaStandalone.class); } public void startup() { for (int i = 0; i < _bufferCount; i++) _emptyBuffers.offer(new AsyncBuffer(_bufferSize)); _isRunning = true; _refillThread = new I2PThread(this, "PRNG"); _refillThread.setDaemon(true); _refillThread.setPriority(Thread.NORM_PRIORITY - 2); _refillThread.start(); } /** * Note - methods may hang or NPE or throw IllegalStateExceptions after this * @since 0.8.8 */ public void shutdown() { _isRunning = false; _emptyBuffers.clear(); _fullBuffers.clear(); _refillThread.interrupt(); // unsynchronized to avoid hanging, may NPE elsewhere _currentBuffer = null; buffer = null; } /** the seed is only propogated once the prng is started with startup() */ @Override public void seed(byte val[]) { Map<String, byte[]> props = Collections.singletonMap(SEED, val); init(props); //fillBlock(); } @Override protected void allocBuffer() {} private static class AsyncBuffer { public final byte[] buffer; public AsyncBuffer(int size) { buffer = new byte[size]; } } /** * make the next available filled buffer current, scheduling any unfilled * buffers for refill, and blocking until at least one buffer is ready */ protected void rotateBuffer() { synchronized (asyncBuffers) { AsyncBuffer old = _currentBuffer; if (old != null) _emptyBuffers.offer(old); long before = System.currentTimeMillis(); AsyncBuffer nextBuffer = null; while (nextBuffer == null) { if (!_isRunning) throw new IllegalStateException("shutdown"); try { nextBuffer = _fullBuffers.take(); } catch (InterruptedException ie) { continue; } } long waited = System.currentTimeMillis()-before; _context.statManager().addRateData("prng.bufferWaitTime", waited, 0); if (waited > 10*1000 && _log.shouldLog(Log.WARN)) _log.warn(Thread.currentThread().getName() + ": Took " + waited + "ms for a full PRNG buffer to be found"); _currentBuffer = nextBuffer; buffer = nextBuffer.buffer; } } /** * The refiller thread */ public void run() { while (_isRunning) { AsyncBuffer aBuff = null; try { aBuff = _emptyBuffers.take(); } catch (InterruptedException ie) { continue; } long before = System.currentTimeMillis(); doFill(aBuff.buffer); long after = System.currentTimeMillis(); boolean shouldWait = _fullBuffers.size() > 1; _fullBuffers.offer(aBuff); _context.statManager().addRateData("prng.bufferFillTime", after - before, 0); if (shouldWait) { Thread.yield(); long waitTime = (after-before)*5; if (waitTime <= 0) // somehow postman saw waitTime show up as negative waitTime = 50; else if (waitTime > 5000) waitTime = 5000; try { Thread.sleep(waitTime); } catch (InterruptedException ie) {} } } } @Override public void fillBlock() { rotateBuffer(); } private void doFill(byte buf[]) { //long start = System.currentTimeMillis(); if (pool0Count >= MIN_POOL_SIZE && System.currentTimeMillis() - lastReseed > 100) { reseedCount++; //byte[] seed = new byte[0]; for (int i = 0; i < NUM_POOLS; i++) { if (reseedCount % (1 << i) == 0) { generator.addRandomBytes(pools[i].digest()); } } lastReseed = System.currentTimeMillis(); } generator.nextBytes(buf); //long now = System.currentTimeMillis(); //long diff = now-lastRefill; //lastRefill = now; //long refillTime = now-start; //System.out.println("Refilling " + (++refillCount) + " after " + diff + " for the PRNG took " + refillTime); } /***** public static void main(String args[]) { try { AsyncFortunaStandalone rand = new AsyncFortunaStandalone(null); // Will cause NPEs above; fix this if you want to test! Sorry... byte seed[] = new byte[1024]; rand.seed(seed); System.out.println("Before starting prng"); rand.startup(); System.out.println("Starting prng, waiting 1 minute"); try { Thread.sleep(60*1000); } catch (InterruptedException ie) {} System.out.println("PRNG started, beginning test"); long before = System.currentTimeMillis(); byte buf[] = new byte[1024]; java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); java.util.zip.GZIPOutputStream gos = new java.util.zip.GZIPOutputStream(baos); for (int i = 0; i < 1024; i++) { rand.nextBytes(buf); gos.write(buf); } long after = System.currentTimeMillis(); gos.finish(); byte compressed[] = baos.toByteArray(); System.out.println("Compressed size of 1MB: " + compressed.length + " took " + (after-before)); } catch (Exception e) { e.printStackTrace(); } try { Thread.sleep(5*60*1000); } catch (InterruptedException ie) {} } *****/ }