/* This code is part of Freenet. It is distributed under the GNU General * Public License, version 2 (or at your option any later version). See * http://www.gnu.org/ for further details of the GPL. */ package freenet.crypt; import static java.util.concurrent.TimeUnit.HOURS; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Properties; import freenet.support.LogThresholdCallback; import freenet.support.Logger; import freenet.support.Logger.LogLevel; import freenet.support.io.Closer; /** * An implementation of the Yarrow PRNG in Java. * <p> * This class implements Yarrow-160, a cryptraphically secure PRNG developed by * John Kelsey, Bruce Schneier, and Neils Ferguson. It was designed to follow * the specification (www.counterpane.com/labs) given in the paper by the same * authors, with the following exceptions: * </p> * <ul> * <li>Instead of 3DES as the output cipher, Rijndael was chosen. It was my * belief that an AES candidate should be selected. Twofish was an alternate * choice, but the AES implementation does not allow easy selection of a faster * key-schedule, so twofish's severely impaired performance.</li> * <li>h prime, described as a 'size adaptor' was not used, since its function * is only to constrain the size of a byte array, our own key generation * routine was used instead (See * {@link freenet.crypt.Util#makeKey freenet.crypt.Util.makeKey})</li> * <li>Our own entropy estimation routines are used, as they use a third-order * delta calculation that is quite conservative. Still, its used along side the * global multiplier and program- supplied guesses, as suggested.</li> * </ul> * * @author Scott G. Miller <scgmille@indiana.edu> */ public class Yarrow extends RandomSource implements PersistentRandomSource { private static final long serialVersionUID = -1; private static volatile boolean logMINOR; static { Logger.registerLogThresholdCallback(new LogThresholdCallback(){ @Override public void shouldUpdate(){ logMINOR = Logger.shouldLog(LogLevel.MINOR, this); } }); } /** * Security parameters */ private static final boolean DEBUG = false; private static final int Pg = 10; private final SecureRandom sr; public final File seedfile; //A file to which seed data should be dumped periodically public Yarrow() { this("prng.seed", "SHA1", "Rijndael", true, true); } public Yarrow(boolean canBlock) { this("prng.seed", "SHA1", "Rijndael", true, canBlock); } public Yarrow(File seed) { this(seed, "SHA1", "Rijndael", true, true); } public Yarrow(String seed, String digest, String cipher, boolean updateSeed, boolean canBlock) { this(new File(seed), digest, cipher, updateSeed, canBlock); } public Yarrow(File seed, String digest, String cipher, boolean updateSeed, boolean canBlock) { this(seed, digest, cipher, updateSeed, canBlock, true); } // unset reseedOnStartup only in unit test Yarrow(File seed, String digest, String cipher, boolean updateSeed, boolean canBlock, boolean reseedOnStartup) { SecureRandom s; try { s = SecureRandom.getInstance("SHA1PRNG"); } catch(NoSuchAlgorithmException e) { s = null; } sr = s; try { accumulator_init(digest); reseed_init(digest); generator_init(cipher); } catch(NoSuchAlgorithmException e) { Logger.error(this, "Could not init pools trying to getInstance(" + digest + "): " + e, e); throw new RuntimeException("Cannot initialize Yarrow!: " + e, e); } if(updateSeed && !(seed.toString()).equals("/dev/urandom")) //Dont try to update the seedfile if we know that it wont be possible anyways seedfile = seed; else seedfile = null; if(reseedOnStartup) { entropy_init(seed, reseedOnStartup); seedFromExternalStuff(canBlock); /** * If we don't reseed at this point, we will be predictable, * because the startup entropy won't cause a reseed. */ fast_pool_reseed(); slow_pool_reseed(); } else { read_seed(seed); } } private void seedFromExternalStuff(boolean canBlock) { byte[] buf = new byte[32]; if(File.separatorChar == '/') { DataInputStream dis = null; FileInputStream fis = null; File hwrng = new File("/dev/hwrng"); if(hwrng.exists() && hwrng.canRead()) try { fis = new FileInputStream(hwrng); dis = new DataInputStream(fis); dis.readFully(buf); consumeBytes(buf); dis.readFully(buf); consumeBytes(buf); dis.close(); } catch(Throwable t) { Logger.normal(this, "Can't read /dev/hwrng even though exists and is readable: " + t, t); } finally { Closer.close(dis); Closer.close(fis); } // Read some bits from /dev/urandom try { fis = new FileInputStream("/dev/urandom"); dis = new DataInputStream(fis); dis.readFully(buf); consumeBytes(buf); dis.readFully(buf); consumeBytes(buf); } catch(Throwable t) { Logger.normal(this, "Can't read /dev/urandom: " + t, t); // We can't read it; let's skip /dev/random and seed from SecureRandom.generateSeed() canBlock = true; } finally { Closer.close(dis); Closer.close(fis); } if(canBlock) // Read some bits from /dev/random try { fis = new FileInputStream("/dev/random"); dis = new DataInputStream(fis); dis.readFully(buf); consumeBytes(buf); dis.readFully(buf); consumeBytes(buf); } catch(Throwable t) { Logger.normal(this, "Can't read /dev/random: " + t, t); } finally { Closer.close(dis); Closer.close(fis); } fis = null; } else // Force generateSeed(), since we can't read random data from anywhere else. // Anyway, Windows's CAPI won't block. canBlock = true; if(canBlock) { // SecureRandom hopefully acts as a proxy for CAPI on Windows buf = sr.generateSeed(32); consumeBytes(buf); buf = sr.generateSeed(32); consumeBytes(buf); } // A few more bits consumeString(Long.toHexString(Runtime.getRuntime().freeMemory())); consumeString(Long.toHexString(Runtime.getRuntime().totalMemory())); } private void entropy_init(File seed, boolean reseedOnStartup) { if(reseedOnStartup) { Properties sys = System.getProperties(); EntropySource startupEntropy = new EntropySource(); // Consume the system properties list for(Enumeration<?> enu = sys.propertyNames(); enu.hasMoreElements();) { String key = (String) enu.nextElement(); consumeString(key); consumeString(sys.getProperty(key)); } // Consume the local IP address try { consumeString(InetAddress.getLocalHost().toString()); } catch(Exception e) { // Ignore } readStartupEntropy(startupEntropy); } read_seed(seed); } protected void readStartupEntropy(EntropySource startupEntropy) { // Consume the current time acceptEntropy(startupEntropy, System.currentTimeMillis(), 0); acceptEntropy(startupEntropy, System.nanoTime(), 0); // Free memory acceptEntropy(startupEntropy, Runtime.getRuntime().freeMemory(), 0); // Total memory acceptEntropy(startupEntropy, Runtime.getRuntime().totalMemory(), 0); } /** * Seed handling */ private void read_seed(File filename) { FileInputStream fis = null; BufferedInputStream bis = null; DataInputStream dis = null; try { fis = new FileInputStream(filename); bis = new BufferedInputStream(fis); dis = new DataInputStream(bis); EntropySource seedFile = new EntropySource(); for(int i = 0; i < 32; i++) acceptEntropy(seedFile, dis.readLong(), 64); dis.close(); } catch(EOFException f) { // Okay. } catch(IOException e) { Logger.error(this, "IOE trying to read the seedfile from disk : " + e.getMessage()); } finally { Closer.close(dis); Closer.close(bis); Closer.close(fis); } fast_pool_reseed(); } private long timeLastWroteSeed = -1; private void write_seed(File filename) { write_seed(filename, false); } /** * {@inheritDoc} */ @Override public void write_seed(boolean force) { write_seed(seedfile, force); } private void write_seed(File filename, boolean force) { if(!force) synchronized(this) { long now = System.currentTimeMillis(); if(now - timeLastWroteSeed <= HOURS.toMillis(1) /* once per hour */) return; else timeLastWroteSeed = now; } FileOutputStream fos = null; BufferedOutputStream bos = null; DataOutputStream dos = null; try { fos = new FileOutputStream(filename); bos = new BufferedOutputStream(fos); dos = new DataOutputStream(bos); for(int i = 0; i < 32; i++) dos.writeLong(nextLong()); dos.flush(); dos.close(); } catch(IOException e) { Logger.error(this, "IOE while saving the seed file! : " + e.getMessage()); } finally { Closer.close(dos); Closer.close(bos); Closer.close(fos); } } /** * 5.1 Generation Mechanism */ private BlockCipher cipher_ctx; private byte[] output_buffer, counter, allZeroString, tmp; private int output_count, fetch_counter; private void generator_init(String cipher) { cipher_ctx = Util.getCipherByName(cipher); output_buffer = new byte[cipher_ctx.getBlockSize() / 8]; counter = new byte[cipher_ctx.getBlockSize() / 8]; allZeroString = new byte[cipher_ctx.getBlockSize() / 8]; tmp = new byte[cipher_ctx.getKeySize() / 8]; fetch_counter = output_buffer.length; } private void counterInc() { for(int i = counter.length - 1; i >= 0; i--) if(++counter[i] != 0) break; } private void generateOutput() { counterInc(); output_buffer = new byte[counter.length]; cipher_ctx.encipher(counter, output_buffer); if(output_count++ > Pg) { output_count = 0; nextBytes(tmp); rekey(tmp); } } private void rekey(byte[] key) { cipher_ctx.initialize(key); counter = new byte[allZeroString.length]; cipher_ctx.encipher(allZeroString, counter); Arrays.fill(key, (byte) 0); } // Fetches count bytes of randomness into the shared buffer, returning // an offset to the bytes private synchronized int getBytes(int count) { if(fetch_counter + count > output_buffer.length) { fetch_counter = 0; generateOutput(); return getBytes(count); } int rv = fetch_counter; fetch_counter += count; return rv; } static final int bitTable[][] = {{0, 0x0}, { 1, 0x1 }, { 1, 0x3 }, { 1, 0x7 }, { 1, 0xf }, { 1, 0x1f }, { 1, 0x3f }, { 1, 0x7f }, { 1, 0xff }, { 2, 0x1ff }, { 2, 0x3ff }, { 2, 0x7ff }, { 2, 0xfff }, { 2, 0x1fff }, { 2, 0x3fff }, { 2, 0x7fff }, { 2, 0xffff }, { 3, 0x1ffff }, { 3, 0x3ffff }, { 3, 0x7ffff }, { 3, 0xfffff }, { 3, 0x1fffff }, { 3, 0x3fffff }, { 3, 0x7fffff }, { 3, 0xffffff }, { 4, 0x1ffffff }, { 4, 0x3ffffff }, { 4, 0x7ffffff }, { 4, 0xfffffff }, { 4, 0x1fffffff }, { 4, 0x3fffffff }, { 4, 0x7fffffff }, { 4, 0xffffffff }}; // This may *look* more complicated than in is, but in fact it is // loop unrolled, cache and operation optimized. // So don't try to simplify it... Thanks. :) // When this was not synchronized, we were getting repeats... @Override protected synchronized int next(int bits) { int[] parameters = bitTable[bits]; int offset = getBytes(parameters[0]); int val = output_buffer[offset]; if(parameters[0] == 4) val += (output_buffer[offset + 1] << 24) + (output_buffer[offset + 2] << 16) + (output_buffer[offset + 3] << 8); else if(parameters[0] == 3) val += (output_buffer[offset + 1] << 16) + (output_buffer[offset + 2] << 8); else if(parameters[0] == 2) val += output_buffer[offset + 2] << 8; return val & parameters[1]; } /** * 5.2 Entropy Accumulator */ private MessageDigest fast_pool, slow_pool; private int fast_entropy, slow_entropy; private boolean fast_select; private Map<EntropySource, int[]> entropySeen; private void accumulator_init(String digest) throws NoSuchAlgorithmException { fast_pool = MessageDigest.getInstance(digest, Util.mdProviders.get(digest)); slow_pool = MessageDigest.getInstance(digest, Util.mdProviders.get(digest)); entropySeen = new HashMap<EntropySource, int[]>(); } @Override public int acceptEntropy(EntropySource source, long data, int entropyGuess) { return acceptEntropy(source, data, entropyGuess, 1.0); } @Override public int acceptEntropyBytes(EntropySource source, byte[] buf, int offset, int length, double bias) { int totalRealEntropy = 0; for(int i = 0; i < length; i += 8) { long thingy = 0; int bytes = 0; for(int j = 0; j < Math.min(length, i + 8); j++) { thingy = (thingy << 8) + (buf[j] & 0xFF); bytes++; } totalRealEntropy += acceptEntropy(source, thingy, bytes * 8, bias); } return totalRealEntropy; } private int acceptEntropy( EntropySource source, long data, int entropyGuess, double bias) { return accept_entropy( data, source, (int) (bias * Math.min( 32, Math.min(estimateEntropy(source, data), entropyGuess)))); } private int accept_entropy(long data, EntropySource source, int actualEntropy) { boolean performedPoolReseed = false; byte[] b = new byte[] { (byte) data, (byte) (data >> 8), (byte) (data >> 16), (byte) (data >> 24), (byte) (data >> 32), (byte) (data >> 40), (byte) (data >> 48), (byte) (data >> 56) }; synchronized(this) { fast_select = !fast_select; MessageDigest pool = (fast_select ? fast_pool : slow_pool); pool.update(b); if(fast_select) { fast_entropy += actualEntropy; if(fast_entropy > FAST_THRESHOLD) { fast_pool_reseed(); performedPoolReseed = true; } } else { slow_entropy += actualEntropy; if(source != null) { int[] contributedEntropy = entropySeen.get(source); if(contributedEntropy == null) { contributedEntropy = new int[] { actualEntropy }; entropySeen.put(source, contributedEntropy); } else contributedEntropy[0]+=actualEntropy; if(slow_entropy >= (SLOW_THRESHOLD * 2)) { int kc = 0; for(Map.Entry<EntropySource, int[]> e : entropySeen.entrySet()) { EntropySource key = e.getKey(); int[] v = e.getValue(); if(DEBUG) Logger.normal(this, "Key: <" + key + "> " + v); if(v[0] > SLOW_THRESHOLD) { kc++; if(kc >= SLOW_K) { slow_pool_reseed(); performedPoolReseed = true; break; } } } } } } if(DEBUG) // Core.logger.log(this,"Fast pool: "+fast_entropy+"\tSlow pool: // "+slow_entropy, LogLevel.NORMAL); System.err.println("Fast pool: " + fast_entropy + "\tSlow pool: " + slow_entropy); } if(performedPoolReseed && (seedfile != null)) { //Dont do this while synchronized on 'this' since //opening a file seems to be suprisingly slow on windows if(logMINOR) Logger.minor(this, "Writing seedfile"); write_seed(seedfile); if(logMINOR) Logger.minor(this, "Written seedfile"); } return actualEntropy; } private int estimateEntropy(EntropySource source, long newVal) { int delta = (int) (newVal - source.lastVal); int delta2 = delta - source.lastDelta; source.lastDelta = delta; int delta3 = delta2 - source.lastDelta2; source.lastDelta2 = delta2; if(delta < 0) delta = -delta; if(delta2 < 0) delta2 = -delta2; if(delta3 < 0) delta3 = -delta3; if(delta > delta2) delta = delta2; if(delta > delta3) delta = delta3; /* * delta is now minimum absolute delta. Round down by 1 bit on general * principles, and limit entropy entimate to 12 bits. */ delta >>= 1; delta &= (1 << 12) - 1; /* Smear msbit right to make an n-bit mask */ delta |= delta >> 8; delta |= delta >> 4; delta |= delta >> 2; delta |= delta >> 1; /* Remove one bit to make this a logarithm */ delta >>= 1; /* Count the bits set in the word */ delta -= (delta >> 1) & 0x555; delta = (delta & 0x333) + ((delta >> 2) & 0x333); delta += (delta >> 4); delta += (delta >> 8); source.lastVal = newVal; return delta & 15; } @Override public int acceptTimerEntropy(EntropySource timer) { return acceptTimerEntropy(timer, 1.0); } @Override public int acceptTimerEntropy(EntropySource timer, double bias) { long now = System.currentTimeMillis(); return acceptEntropy(timer, now - timer.lastVal, 32, bias); } /** * If entropy estimation is supported, this method will block until the * specified number of bits of entropy are available. If estimation isn't * supported, this method will return immediately. */ @Override public void waitForEntropy(int bits) { } /** * 5.3 Reseed mechanism */ private static final int Pt = 5; private MessageDigest reseed_ctx; private void reseed_init(String digest) throws NoSuchAlgorithmException { reseed_ctx = MessageDigest.getInstance(digest, Util.mdProviders.get(digest)); } private void fast_pool_reseed() { long startTime = System.currentTimeMillis(); byte[] v0 = fast_pool.digest(); byte[] vi = v0; for(byte i = 0; i < Pt; i++) { reseed_ctx.update(vi, 0, vi.length); reseed_ctx.update(v0, 0, v0.length); reseed_ctx.update(i); vi = reseed_ctx.digest(); } // vPt=vi Util.makeKey(vi, tmp, 0, tmp.length); rekey(tmp); Arrays.fill(v0, (byte) 0); // blank out for security fast_entropy = 0; if (DEBUG) { long endTime = System.currentTimeMillis(); if(endTime - startTime > 5000) Logger.normal(this, "Fast pool reseed took " + (endTime - startTime) + "ms"); } } private void slow_pool_reseed() { byte[] slow_hash = slow_pool.digest(); fast_pool.update(slow_hash, 0, slow_hash.length); fast_pool_reseed(); slow_entropy = 0; entropySeen.clear(); } /** * 5.4 Reseed Control parameters */ private static final int FAST_THRESHOLD = 100, SLOW_THRESHOLD = 160, SLOW_K = 2; /** * If the RandomSource has any resources it wants to close, it can do so * when this method is called */ @Override public void close() { } /** * Test routine */ public static void main(String[] args) throws Exception { Yarrow r = new Yarrow(new File("/dev/urandom"), "SHA1", "Rijndael", true, false); byte[] b = new byte[1024]; if((args.length == 0) || args[0].equalsIgnoreCase("latency")) { if(args.length == 2) b = new byte[Integer.parseInt(args[1])]; long start = System.currentTimeMillis(); for(int i = 0; i < 100; i++) r.nextBytes(b); System.out.println( (double) (System.currentTimeMillis() - start) / (100 * b.length) * 1024 + " ms/k"); start = System.currentTimeMillis(); for(int i = 0; i < 1000; i++) r.nextInt(); System.out.println( (double) (System.currentTimeMillis() - start) / 1000 + " ms/int"); start = System.currentTimeMillis(); for(int i = 0; i < 1000; i++) r.nextLong(); System.out.println( (double) (System.currentTimeMillis() - start) / 1000 + " ms/long"); } else if(args[0].equalsIgnoreCase("randomness")) { int kb = Integer.parseInt(args[1]); for(int i = 0; i < kb; i++) { r.nextBytes(b); System.out.write(b); } } else if(args[0].equalsIgnoreCase("gathering")) { System.gc(); EntropySource t = new EntropySource(); long start = System.currentTimeMillis(); for(int i = 0; i < 100000; i++) r.acceptEntropy(t, System.currentTimeMillis(), 32); System.err.println( (double) (System.currentTimeMillis() - start) / 100000); System.gc(); start = System.currentTimeMillis(); for(int i = 0; i < 100000; i++) r.acceptTimerEntropy(t); System.err.println( (double) (System.currentTimeMillis() - start) / 100000); } else if(args[0].equalsIgnoreCase("volume")) { b = new byte[1020]; long duration = System.currentTimeMillis() + Integer.parseInt(args[1]); while(System.currentTimeMillis() < duration) { r.nextBytes(b); System.out.write(b); } // } else if (args[0].equals("stream")) { // RandFile f = new RandFile(args[1]); // EntropySource rf = new EntropySource(); // byte[] buffer = new byte[131072]; // while (true) { // r.acceptEntropy(rf, f.nextLong(), 32); // r.nextBytes(buffer); // System.out.write(buffer); // } } else if(args[0].equalsIgnoreCase("bitstream")) while(true) { int v = r.nextInt(); for(int i = 0; i < 32; i++) { if(((v >> i) & 1) == 1) System.out.print('1'); else System.out.print('0'); } } else if(args[0].equalsIgnoreCase("sample")) if((args.length == 1) || args[1].equals("general")) { System.out.println("nextInt(): "); for(int i = 0; i < 3; i++) System.out.println(r.nextInt()); System.out.println("nextLong(): "); for(int i = 0; i < 3; i++) System.out.println(r.nextLong()); System.out.println("nextFloat(): "); for(int i = 0; i < 3; i++) System.out.println(r.nextFloat()); System.out.println("nextDouble(): "); for(int i = 0; i < 3; i++) System.out.println(r.nextDouble()); System.out.println("nextFullFloat(): "); for(int i = 0; i < 3; i++) System.out.println(r.nextFullFloat()); System.out.println("nextFullDouble(): "); for(int i = 0; i < 3; i++) System.out.println(r.nextFullDouble()); } else if(args[1].equals("normalized")) for(int i = 0; i < 20; i++) System.out.println(r.nextDouble()); } private void consumeString(String str) { byte[] b; try { b = str.getBytes("UTF-8"); } catch(UnsupportedEncodingException e) { throw new Error("Impossible: JVM doesn't support UTF-8: " + e, e); } consumeBytes(b); } private void consumeBytes(byte[] bytes) { if(fast_select) fast_pool.update(bytes, 0, bytes.length); else slow_pool.update(bytes, 0, bytes.length); fast_select = !fast_select; } }