/* CSPRNG.java -- continuously-seeded pseudo-random number generator. Copyright (C) 2004, 2006 Free Software Foundation, Inc. This file is a part of GNU Classpath. GNU Classpath 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 2 of the License, or (at your option) any later version. GNU Classpath 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 GNU Classpath; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License cover the whole combination. As a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module. An independent module is a module which is not derived from or based on this library. If you modify this library, you may extend this exception to your version of the library, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. */ package gnu.javax.crypto.prng; import gnu.java.security.Configuration; import gnu.java.security.Properties; import gnu.java.security.Registry; import gnu.java.security.hash.HashFactory; import gnu.java.security.hash.IMessageDigest; import gnu.java.security.prng.BasePRNG; import gnu.java.security.prng.EntropySource; import gnu.java.security.prng.IRandom; import gnu.java.security.prng.LimitReachedException; import gnu.java.security.util.SimpleList; import gnu.java.security.util.Util; import gnu.javax.crypto.cipher.CipherFactory; import gnu.javax.crypto.cipher.IBlockCipher; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.InputStream; import java.io.PrintStream; import java.net.MalformedURLException; import java.net.URL; import java.security.AccessController; import java.security.InvalidKeyException; import java.security.PrivilegedAction; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import java.util.logging.Level; import java.util.logging.Logger; /** * An entropy pool-based pseudo-random number generator based on the PRNG in * Peter Gutmann's cryptlib (http://www.cs.auckland.ac.nz/~pgut001/cryptlib/). *
* The basic properties of this generator are: *
* All properties are of the from name.N, where name * is the name of the source, and N is an integer (staring at 1) that * indicates the preference number for that source. *
* The following vales for name are used here: *
* These properties are file sources, passed as the {@link #FILE_SOURCES} * parameter of the instance. The property value is a 4-tuple formatted as: *
*quality ; offset ; count ; path*
* The parameters are mapped to the parameters defined for {@link * #FILE_SOURCES}. Leading or trailing spaces on any item are trimmed off. *
** These properties are URL sources, passed as the {@link #URL_SOURCES} * parameter of the instance. The property is formatted the same way as file * sources, but the path argument must be a valid URL. *
** These properties are program sources, passed as the {@link * #PROGRAM_SOURCES} parameter of the instance. This property is formatted the * same way as file and URL sources, but the last argument is a program and * its arguments. *
** These properties are other sources, passed as the {@link #OTHER_SOURCES} * parameter of the instance. The property value must be the full name of a * class that implements the {@link EntropySource} interface and has a public * no-argument constructor. *
** Finally, a boolean property "gnu.crypto.csprng.blocking" can be set to the * desired value of {@link #BLOCKING}. *
* An example of valid properties would be: *
* gnu.crypto.csprng.blocking=true * * gnu.crypto.csprng.file.1=75.0;0;256;/dev/random * gnu.crypto.csprng.file.2=10.0;0;100;/home/user/file * * gnu.crypto.csprng.url.1=5.0;0;256;http://www.random.org/cgi-bin/randbyte?nbytes=256 * gnu.crypto.csprng.url.2=0;256;256;http://slashdot.org/ * * gnu.crypto.csprng.program.1=0.5;0;10;last -n 50 * gnu.crypto.csprng.program.2=0.5;0;10;tcpdump -c 5 * * gnu.crypto.csprng.other.1=foo.bar.MyEntropySource * gnu.crypto.csprng.other.2=com.company.OtherEntropySource **/ public static IRandom getSystemInstance() throws ClassNotFoundException, MalformedURLException, NumberFormatException { CSPRNG instance = new CSPRNG(); HashMap attrib = new HashMap(); attrib.put(BLOCKING, Boolean.valueOf(getProperty(BLOCK))); String s = null; // Get each file source "gnu.crypto.csprng.file.N". List l = new LinkedList(); for (int i = 0; (s = getProperty(FILES + i)) != null; i++) try { l.add(parseString(s.trim())); } catch (NumberFormatException nfe) { } attrib.put(FILE_SOURCES, l); l = new LinkedList(); for (int i = 0; (s = getProperty(URLS + i)) != null; i++) try { l.add(parseURL(s.trim())); } catch (NumberFormatException nfe) { } catch (MalformedURLException mue) { } attrib.put(URL_SOURCES, l); l = new LinkedList(); for (int i = 0; (s = getProperty(PROGS + i)) != null; i++) try { l.add(parseString(s.trim())); } catch (NumberFormatException nfe) { } attrib.put(PROGRAM_SOURCES, l); l = new LinkedList(); for (int i = 0; (s = getProperty(OTHER + i)) != null; i++) try { Class c = Class.forName(s.trim()); l.add(c.newInstance()); } catch (ClassNotFoundException cnfe) { } catch (InstantiationException ie) { } catch (IllegalAccessException iae) { } attrib.put(OTHER_SOURCES, l); instance.init(attrib); return instance; } private static String getProperty(final String name) { return (String) AccessController.doPrivileged(new PrivilegedAction() { public Object run() { return Properties.getProperty(name); } }); } private static List parseString(String s) throws NumberFormatException { StringTokenizer tok = new StringTokenizer(s, ";"); if (tok.countTokens() != 4) throw new IllegalArgumentException("malformed property"); Double quality = new Double(tok.nextToken()); Integer offset = new Integer(tok.nextToken()); Integer length = new Integer(tok.nextToken()); String str = tok.nextToken(); return new SimpleList(quality, offset, length, str); } private static List parseURL(String s) throws MalformedURLException, NumberFormatException { StringTokenizer tok = new StringTokenizer(s, ";"); if (tok.countTokens() != 4) throw new IllegalArgumentException("malformed property"); Double quality = new Double(tok.nextToken()); Integer offset = new Integer(tok.nextToken()); Integer length = new Integer(tok.nextToken()); URL url = new URL(tok.nextToken()); return new SimpleList(quality, offset, length, url); } public Object clone() { return new CSPRNG(); } public void setup(Map attrib) { List list = null; if (Configuration.DEBUG) log.fine("attrib=" + String.valueOf(attrib)); try { list = (List) attrib.get(FILE_SOURCES); if (Configuration.DEBUG) log.fine("list=" + String.valueOf(list)); if (list != null) { files.clear(); for (Iterator it = list.iterator(); it.hasNext();) { List l = (List) it.next(); if (Configuration.DEBUG) log.fine("l=" + l); if (l.size() != 4) { if (Configuration.DEBUG) log.fine("file list too small: " + l.size()); throw new IllegalArgumentException("invalid file list"); } Double quality = (Double) l.get(0); Integer offset = (Integer) l.get(1); Integer length = (Integer) l.get(2); String source = (String) l.get(3); files.add(new SimpleList(quality, offset, length, source)); } } } catch (ClassCastException cce) { if (Configuration.DEBUG) log.log(Level.FINE, "bad file list", cce); throw new IllegalArgumentException("invalid file list"); } try { list = (List) attrib.get(URL_SOURCES); if (Configuration.DEBUG) log.fine("list=" + String.valueOf(list)); if (list != null) { urls.clear(); for (Iterator it = list.iterator(); it.hasNext();) { List l = (List) it.next(); if (Configuration.DEBUG) log.fine("l=" + l); if (l.size() != 4) { if (Configuration.DEBUG) log.fine("URL list too small: " + l.size()); throw new IllegalArgumentException("invalid URL list"); } Double quality = (Double) l.get(0); Integer offset = (Integer) l.get(1); Integer length = (Integer) l.get(2); URL source = (URL) l.get(3); urls.add(new SimpleList(quality, offset, length, source)); } } } catch (ClassCastException cce) { if (Configuration.DEBUG) log.log(Level.FINE, "bad URL list", cce); throw new IllegalArgumentException("invalid URL list"); } try { list = (List) attrib.get(PROGRAM_SOURCES); if (Configuration.DEBUG) log.fine("list=" + String.valueOf(list)); if (list != null) { progs.clear(); for (Iterator it = list.iterator(); it.hasNext();) { List l = (List) it.next(); if (Configuration.DEBUG) log.fine("l=" + l); if (l.size() != 4) { if (Configuration.DEBUG) log.fine("program list too small: " + l.size()); throw new IllegalArgumentException("invalid program list"); } Double quality = (Double) l.get(0); Integer offset = (Integer) l.get(1); Integer length = (Integer) l.get(2); String source = (String) l.get(3); progs.add(new SimpleList(quality, offset, length, source)); } } } catch (ClassCastException cce) { if (Configuration.DEBUG) log.log(Level.FINE, "bad program list", cce); throw new IllegalArgumentException("invalid program list"); } try { list = (List) attrib.get(OTHER_SOURCES); if (Configuration.DEBUG) log.fine("list=" + String.valueOf(list)); if (list != null) { other.clear(); for (Iterator it = list.iterator(); it.hasNext();) { EntropySource src = (EntropySource) it.next(); if (Configuration.DEBUG) log.fine("src=" + src); if (src == null) throw new NullPointerException("null source in source list"); other.add(src); } } } catch (ClassCastException cce) { throw new IllegalArgumentException("invalid source list"); } try { Boolean block = (Boolean) attrib.get(BLOCKING); if (block != null) blocking = block.booleanValue(); else blocking = true; } catch (ClassCastException cce) { throw new IllegalArgumentException("invalid blocking parameter"); } poller = new Poller(files, urls, progs, other, this); try { fillBlock(); } catch (LimitReachedException lre) { throw new RuntimeException("bootstrapping CSPRNG failed"); } } public void fillBlock() throws LimitReachedException { if (Configuration.DEBUG) log.fine("fillBlock"); if (getQuality() < 100.0) { if (Configuration.DEBUG) log.fine("doing slow poll"); slowPoll(); } do { fastPoll(); mixRandomPool(); } while (mixCount < MIX_COUNT); if (! x917init || x917count >= X917_LIFETIME) { mixRandomPool(pool); Map attr = new HashMap(); byte[] key = new byte[32]; System.arraycopy(pool, 0, key, 0, 32); cipher.reset(); attr.put(IBlockCipher.KEY_MATERIAL, key); try { cipher.init(attr); } catch (InvalidKeyException ike) { throw new Error(ike.toString()); } mixRandomPool(pool); generateX917(pool); mixRandomPool(pool); generateX917(pool); if (x917init) quality = 0.0; x917init = true; x917count = 0; } byte[] export = new byte[ALLOC_SIZE]; for (int i = 0; i < ALLOC_SIZE; i++) export[i] = (byte)(pool[i] ^ 0xFF); mixRandomPool(); mixRandomPool(export); generateX917(export); for (int i = 0; i < OUTPUT_SIZE; i++) buffer[i] = (byte)(export[i] ^ export[i + OUTPUT_SIZE]); Arrays.fill(export, (byte) 0); } /** * Add an array of bytes into the randomness pool. Note that this method will * not increment the pool's quality counter (this can only be done via * a source provided to the setup method). * * @param buf The byte array. * @param off The offset from whence to start reading bytes. * @param len The number of bytes to add. * @throws ArrayIndexOutOfBoundsException If off or len are * out of the range of buf. */ public synchronized void addRandomBytes(byte[] buf, int off, int len) { if (off < 0 || len < 0 || off + len > buf.length) throw new ArrayIndexOutOfBoundsException(); if (Configuration.DEBUG) { log.fine("adding random bytes:"); log.fine(Util.toString(buf, off, len)); } final int count = off + len; for (int i = off; i < count; i++) { pool[index++] ^= buf[i]; if (index == pool.length) { mixRandomPool(); index = 0; } } } /** * Add a single random byte to the randomness pool. Note that this method will * not increment the pool's quality counter (this can only be done via * a source provided to the setup method). * * @param b The byte to add. */ public synchronized void addRandomByte(byte b) { if (Configuration.DEBUG) log.fine("adding byte " + Integer.toHexString(b)); pool[index++] ^= b; if (index >= pool.length) { mixRandomPool(); index = 0; } } synchronized void addQuality(double quality) { if (Configuration.DEBUG) log.fine("adding quality " + quality); if (this.quality < 100) this.quality += quality; if (Configuration.DEBUG) log.fine("quality now " + this.quality); } synchronized double getQuality() { return quality; } /** * The mix operation. This method will, for every 20-byte block in the random * pool, hash that block, the previous 20 bytes, and the next 44 bytes with * SHA-1, writing the result back into that block. */ private void mixRandomPool(byte[] buf) { int hashSize = hash.hashSize(); for (int i = 0; i < buf.length; i += hashSize) { // First update the bytes [p-19..p-1]. if (i == 0) hash.update(buf, buf.length - hashSize, hashSize); else hash.update(buf, i - hashSize, hashSize); // Now the next 64 bytes. if (i + 64 < buf.length) hash.update(buf, i, 64); else { hash.update(buf, i, buf.length - i); hash.update(buf, 0, 64 - (buf.length - i)); } byte[] digest = hash.digest(); System.arraycopy(digest, 0, buf, i, hashSize); } } private void mixRandomPool() { mixRandomPool(pool); mixCount++; } private void generateX917(byte[] buf) { int off = 0; for (int i = 0; i < buf.length; i += X917_POOL_SIZE) { int copy = Math.min(buf.length - i, X917_POOL_SIZE); for (int j = 0; j < copy; j++) x917pool[j] ^= pool[off + j]; cipher.encryptBlock(x917pool, 0, x917pool, 0); System.arraycopy(x917pool, 0, buf, off, copy); cipher.encryptBlock(x917pool, 0, x917pool, 0); off += copy; x917count++; } } /** * Add random data always immediately available into the random pool, such as * the values of the eight asynchronous counters, the current time, the * current memory usage, the calling thread name, and the current stack trace. *
* This method does not alter the quality counter, and is provided more to * maintain randomness, not to seriously improve the current random state. */ private void fastPoll() { byte b = 0; for (int i = 0; i < SPINNER_COUNT; i++) b ^= SPINNERS[i].counter; addRandomByte(b); addRandomByte((byte) System.currentTimeMillis()); addRandomByte((byte) Runtime.getRuntime().freeMemory()); String s = Thread.currentThread().getName(); if (s != null) { byte[] buf = s.getBytes(); addRandomBytes(buf, 0, buf.length); } ByteArrayOutputStream bout = new ByteArrayOutputStream(1024); PrintStream pout = new PrintStream(bout); Throwable t = new Throwable(); t.printStackTrace(pout); pout.flush(); byte[] buf = bout.toByteArray(); addRandomBytes(buf, 0, buf.length); } private void slowPoll() throws LimitReachedException { if (Configuration.DEBUG) log.fine("poller is alive? " + (pollerThread == null ? false : pollerThread.isAlive())); if (pollerThread == null || ! pollerThread.isAlive()) { boolean interrupted = false; pollerThread = new Thread(poller); pollerThread.setDaemon(true); pollerThread.setPriority(Thread.NORM_PRIORITY - 1); pollerThread.start(); if (blocking) try { pollerThread.join(); } catch (InterruptedException ie) { interrupted = true; } // If the full slow poll has completed after we waited for it, // and there in insufficient randomness, throw an exception. if (! interrupted && blocking && quality < 100.0) { if (Configuration.DEBUG) log.fine("insufficient quality: " + quality); throw new LimitReachedException("insufficient randomness was polled"); } } } protected void finalize() throws Throwable { if (poller != null && pollerThread != null && pollerThread.isAlive()) { pollerThread.interrupt(); poller.stopUpdating(); pollerThread.interrupt(); } Arrays.fill(pool, (byte) 0); Arrays.fill(x917pool, (byte) 0); Arrays.fill(buffer, (byte) 0); } /** * A simple thread that constantly updates a byte counter. This class is used * in a group of lowest-priority threads and the values of their counters * (updated in competition with all other threads) is used as a source of * entropy bits. */ private static class Spinner implements Runnable { protected byte counter; private Spinner() { } public void run() { while (true) { counter++; try { Thread.sleep(100); } catch (InterruptedException ie) { } } } } private final class Poller implements Runnable { private final List files; private final List urls; private final List progs; private final List other; private final CSPRNG pool; private boolean running; Poller(List files, List urls, List progs, List other, CSPRNG pool) { super(); this.files = Collections.unmodifiableList(files); this.urls = Collections.unmodifiableList(urls); this.progs = Collections.unmodifiableList(progs); this.other = Collections.unmodifiableList(other); this.pool = pool; } public void run() { running = true; if (Configuration.DEBUG) { log.fine("files: " + files); log.fine("URLs: " + urls); log.fine("progs: " + progs); } Iterator files_it = files.iterator(); Iterator urls_it = urls.iterator(); Iterator prog_it = progs.iterator(); Iterator other_it = other.iterator(); while (files_it.hasNext() || urls_it.hasNext() || prog_it.hasNext() || other_it.hasNext()) { // There is enough random data. Go away. if (pool.getQuality() >= 100.0 || ! running) return; if (files_it.hasNext()) try { List l = (List) files_it.next(); if (Configuration.DEBUG) log.fine(l.toString()); double qual = ((Double) l.get(0)).doubleValue(); int offset = ((Integer) l.get(1)).intValue(); int count = ((Integer) l.get(2)).intValue(); String src = (String) l.get(3); InputStream in = new FileInputStream(src); byte[] buf = new byte[count]; if (offset > 0) in.skip(offset); int len = in.read(buf); if (len >= 0) { pool.addRandomBytes(buf, 0, len); pool.addQuality(qual * ((double) len / (double) count)); } if (Configuration.DEBUG) log.fine("got " + len + " bytes from " + src); } catch (Exception x) { if (Configuration.DEBUG) log.throwing(this.getClass().getName(), "run", x); } if (pool.getQuality() >= 100.0 || ! running) return; if (urls_it.hasNext()) try { List l = (List) urls_it.next(); if (Configuration.DEBUG) log.fine(l.toString()); double qual = ((Double) l.get(0)).doubleValue(); int offset = ((Integer) l.get(1)).intValue(); int count = ((Integer) l.get(2)).intValue(); URL src = (URL) l.get(3); InputStream in = src.openStream(); byte[] buf = new byte[count]; if (offset > 0) in.skip(offset); int len = in.read(buf); if (len >= 0) { pool.addRandomBytes(buf, 0, len); pool.addQuality(qual * ((double) len / (double) count)); } if (Configuration.DEBUG) log.fine("got " + len + " bytes from " + src); } catch (Exception x) { if (Configuration.DEBUG) log.throwing(this.getClass().getName(), "run", x); } if (pool.getQuality() >= 100.0 || ! running) return; Process proc = null; if (prog_it.hasNext()) try { List l = (List) prog_it.next(); if (Configuration.DEBUG) log.finer(l.toString()); double qual = ((Double) l.get(0)).doubleValue(); int offset = ((Integer) l.get(1)).intValue(); int count = ((Integer) l.get(2)).intValue(); String src = (String) l.get(3); proc = null; proc = Runtime.getRuntime().exec(src); InputStream in = proc.getInputStream(); byte[] buf = new byte[count]; if (offset > 0) in.skip(offset); int len = in.read(buf); if (len >= 0) { pool.addRandomBytes(buf, 0, len); pool.addQuality(qual * ((double) len / (double) count)); } proc.destroy(); proc.waitFor(); if (Configuration.DEBUG) log.fine("got " + len + " bytes from " + src); } catch (Exception x) { if (Configuration.DEBUG) log.throwing(this.getClass().getName(), "run", x); try { if (proc != null) { proc.destroy(); proc.waitFor(); } } catch (Exception ignored) { } } if (pool.getQuality() >= 100.0 || ! running) return; if (other_it.hasNext()) try { EntropySource src = (EntropySource) other_it.next(); byte[] buf = src.nextBytes(); if (pool == null) return; pool.addRandomBytes(buf, 0, buf.length); pool.addQuality(src.quality()); if (Configuration.DEBUG) log.fine("got " + buf.length + " bytes from " + src); } catch (Exception x) { if (Configuration.DEBUG) log.throwing(this.getClass().getName(), "run", x); } } } public void stopUpdating() { running = false; } } }