/* 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 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.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.Hashtable; import java.util.Properties; import thaw.core.Logger; /** * 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 { private static final long serialVersionUID = -1; /** * Security parameters */ private static final boolean DEBUG = false; private static final int Pg = 10; private final SecureRandom sr; private final File seedfile; //A file to which seed data should be dumped periodically public Yarrow() { this("prng.seed", "SHA1", "Rijndael",true); } public Yarrow(String seed, String digest, String cipher,boolean updateSeed) { this(new File(seed), digest, cipher,updateSeed); } public Yarrow(File seed, String digest, String cipher,boolean updateSeed) { 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.toString()); e.printStackTrace(); throw new RuntimeException("Cannot initialize Yarrow!: "+e, e); } entropy_init(seed); seedFromExternalStuff(false); 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 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(); } public 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); } catch (Throwable t) { Logger.notice(this, "Can't read /dev/hwrng even though exists and is readable: "+t.toString()); } finally { try { if(fis != null) fis.close(); if(dis != null) dis.close(); } catch (IOException e) {} } } // 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.notice(this, "Can't read /dev/urandom: "+t); } finally { try { if(fis != null) fis.close(); if(dis != null) dis.close(); } catch (IOException e) {} } 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.notice(this, "Can't read /dev/random: "+t); } finally { try { if(fis != null) fis.close(); if(dis != null) dis.close(); } catch (IOException e) {} } } 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) { 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); // Free memory acceptEntropy(startupEntropy, Runtime.getRuntime().freeMemory(), 0); // Total memory acceptEntropy(startupEntropy, Runtime.getRuntime().totalMemory(), 0); } /** * Seed handling */ private void read_seed(File filename) { try { DataInputStream dis = new DataInputStream(new FileInputStream(filename)); EntropySource seedFile = new EntropySource(); try { for (int i = 0; i < 32; i++) acceptEntropy(seedFile, dis.readLong(), 64); } catch (EOFException f) { } dis.close(); } catch (Exception e) { } fast_pool_reseed(); } private long timeLastWroteSeed = -1; private void write_seed(File filename) { synchronized(this) { long now = System.currentTimeMillis(); if(now - timeLastWroteSeed <= 60*1000) { return; } else timeLastWroteSeed = now; } try { DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(filename))); for (int i = 0; i < 32; i++) dos.writeLong(nextLong()); dos.close(); } catch (Exception e) { } } /** * 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 final void counterInc() { for (int i = counter.length - 1; i >= 0; i--) if (++counter[i] != 0) break; } private final 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... 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 Hashtable entropySeen; private void accumulator_init(String digest) throws NoSuchAlgorithmException { fast_pool = MessageDigest.getInstance(digest); slow_pool = MessageDigest.getInstance(digest); entropySeen = new Hashtable(); } public int acceptEntropy(EntropySource source, long data, int entropyGuess) { return acceptEntropy(source, data, entropyGuess, 1.0); } 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]; bytes++; } totalRealEntropy += acceptEntropy(source, thingy, bytes*8, bias); } return totalRealEntropy; } public 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; synchronized (this) { fast_select = !fast_select; MessageDigest pool = (fast_select ? fast_pool : slow_pool); pool.update((byte) data); pool.update((byte) (data >> 8)); pool.update((byte) (data >> 16)); pool.update((byte) (data >> 24)); pool.update((byte) (data >> 32)); pool.update((byte) (data >> 40)); pool.update((byte) (data >> 48)); pool.update((byte) (data >> 56)); if (fast_select) { fast_entropy += actualEntropy; if (fast_entropy > FAST_THRESHOLD) { fast_pool_reseed(); performedPoolReseed = true; } } else { slow_entropy += actualEntropy; if (source != null) { Integer contributedEntropy = (Integer) entropySeen.get(source); if (contributedEntropy == null) contributedEntropy = new Integer(actualEntropy); else contributedEntropy = new Integer(actualEntropy + contributedEntropy.intValue()); entropySeen.put(source, contributedEntropy); if (slow_entropy >= (SLOW_THRESHOLD * 2)) { int kc = 0; for (Enumeration enu = entropySeen.keys(); enu.hasMoreElements();) { Object key = enu.nextElement(); Integer v = (Integer) entropySeen.get(key); if (DEBUG) Logger.info(this, "Key: <" + key + "> " + v); if (v.intValue() > 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, Logger.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 Logger.debug(this, "Writing seedfile"); write_seed(seedfile); Logger.debug(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; } public int acceptTimerEntropy(EntropySource timer) { return acceptTimerEntropy(timer, 1.0); } 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. */ 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); } 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; long endTime = System.currentTimeMillis(); if(endTime - startTime > 5000) Logger.debug(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; Integer ZERO = new Integer(0); for (Enumeration enu = entropySeen.keys(); enu.hasMoreElements();) entropySeen.put(enu.nextElement(), ZERO); } /** * 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 */ public void close() { } /** * Test routine */ public static void main(String[] args) throws Exception { Yarrow r = new Yarrow(new File("/dev/urandom"), "SHA1", "Rijndael",true); 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 = str.getBytes(); 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; } public String getCheckpointName() { return "Yarrow random number generator checkpoint"; } public long nextCheckpoint() { return System.currentTimeMillis() + 60*60*1000; } public void checkpoint() { seedFromExternalStuff(true); } }