| /* |
| * This software is provided 'as-is', without any express or implied |
| * warranty. In no event will Google be held liable for any damages |
| * arising from the use of this software. |
| * |
| * Permission is granted to anyone to use this software for any purpose, |
| * including commercial applications, and to alter it and redistribute it |
| * freely, as long as the origin is not misrepresented. |
| */ |
| |
| package test.pkg; |
| |
| import android.os.Build; |
| import android.os.Process; |
| import android.util.Log; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.DataInputStream; |
| import java.io.DataOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.Provider; |
| import java.security.SecureRandom; |
| import java.security.SecureRandomSpi; |
| import java.security.Security; |
| |
| /** |
| * Fixes for the output of the default PRNG having low entropy. |
| * |
| * The fixes need to be applied via {@link #apply()} before any use of Java |
| * Cryptography Architecture primitives. A good place to invoke them is in the |
| * application's {@code onCreate}. |
| */ |
| public final class PrngWorkaround { |
| |
| private static final int VERSION_CODE_JELLY_BEAN = 16; |
| private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18; |
| private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = |
| getBuildFingerprintAndDeviceSerial(); |
| |
| /** Hidden constructor to prevent instantiation. */ |
| private PrngWorkaround() {} |
| |
| /** |
| * Applies all fixes. |
| * |
| * @throws SecurityException if a fix is needed but could not be applied. |
| */ |
| public static void apply() { |
| applyOpenSSLFix(); |
| installLinuxPRNGSecureRandom(); |
| } |
| |
| /** |
| * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the |
| * fix is not needed. |
| * |
| * @throws SecurityException if the fix is needed but could not be applied. |
| */ |
| private static void applyOpenSSLFix() throws SecurityException { |
| if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN) |
| || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) { |
| // No need to apply the fix |
| return; |
| } |
| |
| try { |
| // Mix in the device- and invocation-specific seed. |
| Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") |
| .getMethod("RAND_seed", byte[].class) |
| .invoke(null, generateSeed()); |
| |
| // Mix output of Linux PRNG into OpenSSL's PRNG |
| int bytesRead = (Integer) Class.forName( |
| "org.apache.harmony.xnet.provider.jsse.NativeCrypto") |
| .getMethod("RAND_load_file", String.class, long.class) |
| .invoke(null, "/dev/urandom", 1024); |
| if (bytesRead != 1024) { |
| throw new IOException( |
| "Unexpected number of bytes read from Linux PRNG: " |
| + bytesRead); |
| } |
| } catch (Exception e) { |
| throw new SecurityException("Failed to seed OpenSSL PRNG", e); |
| } |
| } |
| |
| /** |
| * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the |
| * default. Does nothing if the implementation is already the default or if |
| * there is not need to install the implementation. |
| * |
| * @throws SecurityException if the fix is needed but could not be applied. |
| */ |
| private static void installLinuxPRNGSecureRandom() |
| throws SecurityException { |
| if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) { |
| // No need to apply the fix |
| return; |
| } |
| |
| // Install a Linux PRNG-based SecureRandom implementation as the |
| // default, if not yet installed. |
| Provider[] secureRandomProviders = |
| Security.getProviders("SecureRandom.SHA1PRNG"); |
| if ((secureRandomProviders == null) |
| || (secureRandomProviders.length < 1) |
| || (!LinuxPRNGSecureRandomProvider.class.equals( |
| secureRandomProviders[0].getClass()))) { |
| Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); |
| } |
| |
| // Assert that new SecureRandom() and |
| // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed |
| // by the Linux PRNG-based SecureRandom implementation. |
| SecureRandom rng1 = new SecureRandom(); |
| if (!LinuxPRNGSecureRandomProvider.class.equals( |
| rng1.getProvider().getClass())) { |
| throw new SecurityException( |
| "new SecureRandom() backed by wrong Provider: " |
| + rng1.getProvider().getClass()); |
| } |
| |
| SecureRandom rng2; |
| try { |
| rng2 = SecureRandom.getInstance("SHA1PRNG"); |
| } catch (NoSuchAlgorithmException e) { |
| throw new SecurityException("SHA1PRNG not available", e); |
| } |
| if (!LinuxPRNGSecureRandomProvider.class.equals( |
| rng2.getProvider().getClass())) { |
| throw new SecurityException( |
| "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong" |
| + " Provider: " + rng2.getProvider().getClass()); |
| } |
| } |
| |
| /** |
| * {@code Provider} of {@code SecureRandom} engines which pass through |
| * all requests to the Linux PRNG. |
| */ |
| private static class LinuxPRNGSecureRandomProvider extends Provider { |
| |
| public LinuxPRNGSecureRandomProvider() { |
| super("LinuxPRNG", |
| 1.0, |
| "A Linux-specific random number provider that uses" |
| + " /dev/urandom"); |
| // Although /dev/urandom is not a SHA-1 PRNG, some apps |
| // explicitly request a SHA1PRNG SecureRandom and we thus need to |
| // prevent them from getting the default implementation whose output |
| // may have low entropy. |
| put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); |
| put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); |
| } |
| } |
| |
| /** |
| * {@link SecureRandomSpi} which passes all requests to the Linux PRNG |
| * ({@code /dev/urandom}). |
| */ |
| public static class LinuxPRNGSecureRandom extends SecureRandomSpi { |
| |
| /* |
| * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed |
| * are passed through to the Linux PRNG (/dev/urandom). Instances of |
| * this class seed themselves by mixing in the current time, PID, UID, |
| * build fingerprint, and hardware serial number (where available) into |
| * Linux PRNG. |
| * |
| * Concurrency: Read requests to the underlying Linux PRNG are |
| * serialized (on sLock) to ensure that multiple threads do not get |
| * duplicated PRNG output. |
| */ |
| |
| private static final File URANDOM_FILE = new File("/dev/urandom"); |
| |
| private static final Object sLock = new Object(); |
| |
| /** |
| * Input stream for reading from Linux PRNG or {@code null} if not yet |
| * opened. |
| * |
| * @GuardedBy("sLock") |
| */ |
| private static DataInputStream sUrandomIn; |
| |
| /** |
| * Output stream for writing to Linux PRNG or {@code null} if not yet |
| * opened. |
| * |
| * @GuardedBy("sLock") |
| */ |
| private static OutputStream sUrandomOut; |
| |
| /** |
| * Whether this engine instance has been seeded. This is needed because |
| * each instance needs to seed itself if the client does not explicitly |
| * seed it. |
| */ |
| private boolean mSeeded; |
| |
| @Override |
| protected void engineSetSeed(byte[] bytes) { |
| try { |
| OutputStream out; |
| synchronized (sLock) { |
| out = getUrandomOutputStream(); |
| } |
| out.write(bytes); |
| out.flush(); |
| } catch (IOException e) { |
| // On a small fraction of devices /dev/urandom is not writable. |
| // Log and ignore. |
| Log.w(PrngWorkaround.class.getSimpleName(), |
| "Failed to mix seed into " + URANDOM_FILE); |
| } finally { |
| mSeeded = true; |
| } |
| } |
| |
| @Override |
| protected void engineNextBytes(byte[] bytes) { |
| if (!mSeeded) { |
| // Mix in the device- and invocation-specific seed. |
| engineSetSeed(generateSeed()); |
| } |
| |
| try { |
| DataInputStream in; |
| synchronized (sLock) { |
| in = getUrandomInputStream(); |
| } |
| synchronized (in) { |
| in.readFully(bytes); |
| } |
| } catch (IOException e) { |
| throw new SecurityException( |
| "Failed to read from " + URANDOM_FILE, e); |
| } |
| } |
| |
| @Override |
| protected byte[] engineGenerateSeed(int size) { |
| byte[] seed = new byte[size]; |
| engineNextBytes(seed); |
| return seed; |
| } |
| |
| private DataInputStream getUrandomInputStream() { |
| synchronized (sLock) { |
| if (sUrandomIn == null) { |
| // NOTE: Consider inserting a BufferedInputStream between |
| // DataInputStream and FileInputStream if you need higher |
| // PRNG output performance and can live with future PRNG |
| // output being pulled into this process prematurely. |
| try { |
| sUrandomIn = new DataInputStream( |
| new FileInputStream(URANDOM_FILE)); |
| } catch (IOException e) { |
| throw new SecurityException("Failed to open " |
| + URANDOM_FILE + " for reading", e); |
| } |
| } |
| return sUrandomIn; |
| } |
| } |
| |
| private OutputStream getUrandomOutputStream() throws IOException { |
| synchronized (sLock) { |
| if (sUrandomOut == null) { |
| sUrandomOut = new FileOutputStream(URANDOM_FILE); |
| } |
| return sUrandomOut; |
| } |
| } |
| } |
| |
| /** |
| * Generates a device- and invocation-specific seed to be mixed into the |
| * Linux PRNG. |
| */ |
| private static byte[] generateSeed() { |
| try { |
| ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); |
| DataOutputStream seedBufferOut = |
| new DataOutputStream(seedBuffer); |
| seedBufferOut.writeLong(System.currentTimeMillis()); |
| seedBufferOut.writeLong(System.nanoTime()); |
| seedBufferOut.writeInt(Process.myPid()); |
| seedBufferOut.writeInt(Process.myUid()); |
| seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); |
| seedBufferOut.close(); |
| return seedBuffer.toByteArray(); |
| } catch (IOException e) { |
| throw new SecurityException("Failed to generate seed", e); |
| } |
| } |
| |
| /** |
| * Gets the hardware serial number of this device. |
| * |
| * @return serial number or {@code null} if not available. |
| */ |
| private static String getDeviceSerialNumber() { |
| // We're using the Reflection API because Build.SERIAL is only available |
| // since API Level 9 (Gingerbread, Android 2.3). |
| try { |
| return (String) Build.class.getField("SERIAL").get(null); |
| } catch (Exception ignored) { |
| return null; |
| } |
| } |
| |
| private static byte[] getBuildFingerprintAndDeviceSerial() { |
| StringBuilder result = new StringBuilder(); |
| String fingerprint = Build.FINGERPRINT; |
| if (fingerprint != null) { |
| result.append(fingerprint); |
| } |
| String serial = getDeviceSerialNumber(); |
| if (serial != null) { |
| result.append(serial); |
| } |
| try { |
| return result.toString().getBytes("UTF-8"); |
| } catch (UnsupportedEncodingException e) { |
| throw new RuntimeException("UTF-8 encoding not supported"); |
| } |
| } |
| } |