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