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}