PRNGFixes.java

  1package eu.siacs.conversations.utils;
  2
  3import android.os.Build;
  4import android.os.Process;
  5import android.util.Log;
  6
  7import java.io.ByteArrayOutputStream;
  8import java.io.DataInputStream;
  9import java.io.DataOutputStream;
 10import java.io.File;
 11import java.io.FileInputStream;
 12import java.io.FileOutputStream;
 13import java.io.IOException;
 14import java.io.OutputStream;
 15import java.io.UnsupportedEncodingException;
 16import java.security.NoSuchAlgorithmException;
 17import java.security.Provider;
 18import java.security.SecureRandom;
 19import java.security.SecureRandomSpi;
 20import java.security.Security;
 21
 22/**
 23 * Fixes for the output of the default PRNG having low entropy.
 24 * 
 25 * The fixes need to be applied via {@link #apply()} before any use of Java
 26 * Cryptography Architecture primitives. A good place to invoke them is in the
 27 * application's {@code onCreate}.
 28 */
 29public final class PRNGFixes {
 30
 31	private static final int VERSION_CODE_JELLY_BEAN = 16;
 32	private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
 33	private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = getBuildFingerprintAndDeviceSerial();
 34
 35	/** Hidden constructor to prevent instantiation. */
 36	private PRNGFixes() {
 37	}
 38
 39	/**
 40	 * Applies all fixes.
 41	 * 
 42	 * @throws SecurityException
 43	 *             if a fix is needed but could not be applied.
 44	 */
 45	public static void apply() {
 46		applyOpenSSLFix();
 47		installLinuxPRNGSecureRandom();
 48	}
 49
 50	/**
 51	 * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
 52	 * fix is not needed.
 53	 * 
 54	 * @throws SecurityException
 55	 *             if the fix is needed but could not be applied.
 56	 */
 57	private static void applyOpenSSLFix() throws SecurityException {
 58		if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN)
 59				|| (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
 60			// No need to apply the fix
 61			return;
 62		}
 63
 64		try {
 65			// Mix in the device- and invocation-specific seed.
 66			Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
 67					.getMethod("RAND_seed", byte[].class)
 68					.invoke(null, generateSeed());
 69
 70			// Mix output of Linux PRNG into OpenSSL's PRNG
 71			int bytesRead = (Integer) Class
 72					.forName(
 73							"org.apache.harmony.xnet.provider.jsse.NativeCrypto")
 74					.getMethod("RAND_load_file", String.class, long.class)
 75					.invoke(null, "/dev/urandom", 1024);
 76			if (bytesRead != 1024) {
 77				throw new IOException(
 78						"Unexpected number of bytes read from Linux PRNG: "
 79								+ bytesRead);
 80			}
 81		} catch (Exception e) {
 82			throw new SecurityException("Failed to seed OpenSSL PRNG", e);
 83		}
 84	}
 85
 86	/**
 87	 * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
 88	 * default. Does nothing if the implementation is already the default or if
 89	 * there is not need to install the implementation.
 90	 * 
 91	 * @throws SecurityException
 92	 *             if the fix is needed but could not be applied.
 93	 */
 94	private static void installLinuxPRNGSecureRandom() throws SecurityException {
 95		if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
 96			// No need to apply the fix
 97			return;
 98		}
 99
100		// Install a Linux PRNG-based SecureRandom implementation as the
101		// default, if not yet installed.
102		Provider[] secureRandomProviders = Security
103				.getProviders("SecureRandom.SHA1PRNG");
104		if ((secureRandomProviders == null)
105				|| (secureRandomProviders.length < 1)
106				|| (!LinuxPRNGSecureRandomProvider.class
107						.equals(secureRandomProviders[0].getClass()))) {
108			Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
109		}
110
111		// Assert that new SecureRandom() and
112		// SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
113		// by the Linux PRNG-based SecureRandom implementation.
114		SecureRandom rng1 = new SecureRandom();
115		if (!LinuxPRNGSecureRandomProvider.class.equals(rng1.getProvider()
116				.getClass())) {
117			throw new SecurityException(
118					"new SecureRandom() backed by wrong Provider: "
119							+ rng1.getProvider().getClass());
120		}
121
122		SecureRandom rng2;
123		try {
124			rng2 = SecureRandom.getInstance("SHA1PRNG");
125		} catch (NoSuchAlgorithmException e) {
126			throw new SecurityException("SHA1PRNG not available", e);
127		}
128		if (!LinuxPRNGSecureRandomProvider.class.equals(rng2.getProvider()
129				.getClass())) {
130			throw new SecurityException(
131					"SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
132							+ " Provider: " + rng2.getProvider().getClass());
133		}
134	}
135
136	/**
137	 * {@code Provider} of {@code SecureRandom} engines which pass through all
138	 * requests to the Linux PRNG.
139	 */
140	private static class LinuxPRNGSecureRandomProvider extends Provider {
141
142		public LinuxPRNGSecureRandomProvider() {
143			super("LinuxPRNG", 1.0,
144					"A Linux-specific random number provider that uses"
145							+ " /dev/urandom");
146			// Although /dev/urandom is not a SHA-1 PRNG, some apps
147			// explicitly request a SHA1PRNG SecureRandom and we thus need to
148			// prevent them from getting the default implementation whose output
149			// may have low entropy.
150			put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
151			put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
152		}
153	}
154
155	/**
156	 * {@link SecureRandomSpi} which passes all requests to the Linux PRNG (
157	 * {@code /dev/urandom}).
158	 */
159	public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
160
161		/*
162		 * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
163		 * are passed through to the Linux PRNG (/dev/urandom). Instances of
164		 * this class seed themselves by mixing in the current time, PID, UID,
165		 * build fingerprint, and hardware serial number (where available) into
166		 * Linux PRNG.
167		 * 
168		 * Concurrency: Read requests to the underlying Linux PRNG are
169		 * serialized (on sLock) to ensure that multiple threads do not get
170		 * duplicated PRNG output.
171		 */
172
173		private static final File URANDOM_FILE = new File("/dev/urandom");
174
175		private static final Object sLock = new Object();
176
177		/**
178		 * Input stream for reading from Linux PRNG or {@code null} if not yet
179		 * opened.
180		 * 
181		 * @GuardedBy("sLock")
182		 */
183		private static DataInputStream sUrandomIn;
184
185		/**
186		 * Output stream for writing to Linux PRNG or {@code null} if not yet
187		 * opened.
188		 * 
189		 * @GuardedBy("sLock")
190		 */
191		private static OutputStream sUrandomOut;
192
193		/**
194		 * Whether this engine instance has been seeded. This is needed because
195		 * each instance needs to seed itself if the client does not explicitly
196		 * seed it.
197		 */
198		private boolean mSeeded;
199
200		@Override
201		protected void engineSetSeed(byte[] bytes) {
202			try {
203				OutputStream out;
204				synchronized (sLock) {
205					out = getUrandomOutputStream();
206				}
207				out.write(bytes);
208				out.flush();
209			} catch (IOException e) {
210				// On a small fraction of devices /dev/urandom is not writable.
211				// Log and ignore.
212				Log.w(PRNGFixes.class.getSimpleName(),
213						"Failed to mix seed into " + URANDOM_FILE);
214			} finally {
215				mSeeded = true;
216			}
217		}
218
219		@Override
220		protected void engineNextBytes(byte[] bytes) {
221			if (!mSeeded) {
222				// Mix in the device- and invocation-specific seed.
223				engineSetSeed(generateSeed());
224			}
225
226			try {
227				DataInputStream in;
228				synchronized (sLock) {
229					in = getUrandomInputStream();
230				}
231				synchronized (in) {
232					in.readFully(bytes);
233				}
234			} catch (IOException e) {
235				throw new SecurityException("Failed to read from "
236						+ URANDOM_FILE, e);
237			}
238		}
239
240		@Override
241		protected byte[] engineGenerateSeed(int size) {
242			byte[] seed = new byte[size];
243			engineNextBytes(seed);
244			return seed;
245		}
246
247		private DataInputStream getUrandomInputStream() {
248			synchronized (sLock) {
249				if (sUrandomIn == null) {
250					// NOTE: Consider inserting a BufferedInputStream between
251					// DataInputStream and FileInputStream if you need higher
252					// PRNG output performance and can live with future PRNG
253					// output being pulled into this process prematurely.
254					try {
255						sUrandomIn = new DataInputStream(new FileInputStream(
256								URANDOM_FILE));
257					} catch (IOException e) {
258						throw new SecurityException("Failed to open "
259								+ URANDOM_FILE + " for reading", e);
260					}
261				}
262				return sUrandomIn;
263			}
264		}
265
266		private OutputStream getUrandomOutputStream() throws IOException {
267			synchronized (sLock) {
268				if (sUrandomOut == null) {
269					sUrandomOut = new FileOutputStream(URANDOM_FILE);
270				}
271				return sUrandomOut;
272			}
273		}
274	}
275
276	/**
277	 * Generates a device- and invocation-specific seed to be mixed into the
278	 * Linux PRNG.
279	 */
280	private static byte[] generateSeed() {
281		try {
282			ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
283			DataOutputStream seedBufferOut = new DataOutputStream(seedBuffer);
284			seedBufferOut.writeLong(System.currentTimeMillis());
285			seedBufferOut.writeLong(System.nanoTime());
286			seedBufferOut.writeInt(Process.myPid());
287			seedBufferOut.writeInt(Process.myUid());
288			seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
289			seedBufferOut.close();
290			return seedBuffer.toByteArray();
291		} catch (IOException e) {
292			throw new SecurityException("Failed to generate seed", e);
293		}
294	}
295
296	/**
297	 * Gets the hardware serial number of this device.
298	 * 
299	 * @return serial number or {@code null} if not available.
300	 */
301	private static String getDeviceSerialNumber() {
302		// We're using the Reflection API because Build.SERIAL is only available
303		// since API Level 9 (Gingerbread, Android 2.3).
304		try {
305			return (String) Build.class.getField("SERIAL").get(null);
306		} catch (Exception ignored) {
307			return null;
308		}
309	}
310
311	private static byte[] getBuildFingerprintAndDeviceSerial() {
312		StringBuilder result = new StringBuilder();
313		String fingerprint = Build.FINGERPRINT;
314		if (fingerprint != null) {
315			result.append(fingerprint);
316		}
317		String serial = getDeviceSerialNumber();
318		if (serial != null) {
319			result.append(serial);
320		}
321		try {
322			return result.toString().getBytes("UTF-8");
323		} catch (UnsupportedEncodingException e) {
324			throw new RuntimeException("UTF-8 encoding not supported");
325		}
326	}
327}