Skip to content

Commit 5456ace

Browse files
author
Kannan Goundan
committed
Add a workaround for older Android versions' buggy SecureRandom.
1 parent 206496b commit 5456ace

3 files changed

Lines changed: 267 additions & 1 deletion

File tree

ChangeLog.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
- Add a workaround for older Android versions' buggy SecureRandom.
2+
13
---------------------------------------------
24
2.0-beta-2 (2015-11-13)
35
- Put "Dbx" prefix on namespace classes (Files -> DbxFiles, etc.)

src/com/dropbox/core/android/AuthActivity.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.dropbox.core.android;
22

33
import java.security.SecureRandom;
4+
import java.security.SecureRandomSpi;
45
import java.util.List;
56
import java.util.Locale;
67

@@ -143,7 +144,7 @@ public interface SecurityProvider {
143144
private static SecurityProvider sSecurityProvider = new SecurityProvider() {
144145
@Override
145146
public SecureRandom getSecureRandom() {
146-
return new SecureRandom();
147+
return FixedSecureRandom.get();
147148
}
148149
};
149150
private static final Object sSecurityProviderLock = new Object();
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package com.dropbox.core.android;
2+
3+
/*
4+
* This software is provided 'as-is', without any express or implied
5+
* warranty. In no event will Google be held liable for any damages
6+
* arising from the use of this software.
7+
*
8+
* Permission is granted to anyone to use this software for any purpose,
9+
* including commercial applications, and to alter it and redistribute it
10+
* freely, as long as the origin is not misrepresented.
11+
*/
12+
13+
import android.os.Build;
14+
import android.os.Process;
15+
import android.util.Log;
16+
17+
import java.io.ByteArrayOutputStream;
18+
import java.io.DataInputStream;
19+
import java.io.DataOutputStream;
20+
import java.io.File;
21+
import java.io.FileInputStream;
22+
import java.io.FileOutputStream;
23+
import java.io.IOException;
24+
import java.io.OutputStream;
25+
import java.io.UnsupportedEncodingException;
26+
import java.security.Provider;
27+
import java.security.SecureRandom;
28+
import java.security.SecureRandomSpi;
29+
30+
/**
31+
* Older versions of Android have a SecureRandom that isn't actually secure. This
32+
* class implements a workaround. Call the static {@link #get()} method to get a
33+
* secure SecureRandom instance.
34+
*
35+
* <p>
36+
* This workaround code was recommended by Google in a
37+
* <a href="http://android-developers.blogspot.com.es/2013/08/some-securerandom-thoughts.html">euphemistically-titled blog post</a>.
38+
* Our code is slightly different because we're a library so we don't want to change
39+
* global JVM settings.
40+
* </p>
41+
*/
42+
public final class FixedSecureRandom extends SecureRandom
43+
{
44+
public static SecureRandom get()
45+
{
46+
if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
47+
// This version of Android doesn't have the issue.
48+
return new SecureRandom();
49+
} else {
50+
return new FixedSecureRandom();
51+
}
52+
}
53+
54+
private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
55+
private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = getBuildFingerprintAndDeviceSerial();
56+
57+
private FixedSecureRandom()
58+
{
59+
super(new LinuxPrngSecureRandomSpi(), new LinuxPrngSecureRandomProvider());
60+
}
61+
62+
/**
63+
* {@code Provider} of {@code SecureRandom} engines which pass through
64+
* all requests to the Linux PRNG.
65+
*/
66+
private static class LinuxPrngSecureRandomProvider extends Provider
67+
{
68+
public LinuxPrngSecureRandomProvider()
69+
{
70+
super("LinuxPRNG",
71+
1.0,
72+
"A Linux-specific random number provider that uses"
73+
+ " /dev/urandom");
74+
// Although /dev/urandom is not a SHA-1 PRNG, some apps
75+
// explicitly request a SHA1PRNG SecureRandom and we thus need to
76+
// prevent them from getting the default implementation whose output
77+
// may have low entropy.
78+
put("SecureRandom.SHA1PRNG", LinuxPrngSecureRandomSpi.class.getName());
79+
put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
80+
}
81+
}
82+
83+
/**
84+
* {@link SecureRandomSpi} which passes all requests to the Linux PRNG
85+
* ({@code /dev/urandom}).
86+
*/
87+
public static class LinuxPrngSecureRandomSpi extends SecureRandomSpi
88+
{
89+
/*
90+
* IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
91+
* are passed through to the Linux PRNG (/dev/urandom). Instances of
92+
* this class seed themselves by mixing in the current time, PID, UID,
93+
* build fingerprint, and hardware serial number (where available) into
94+
* Linux PRNG.
95+
*
96+
* Concurrency: Read requests to the underlying Linux PRNG are
97+
* serialized (on sLock) to ensure that multiple threads do not get
98+
* duplicated PRNG output.
99+
*/
100+
101+
private static final File URANDOM_FILE = new File("/dev/urandom");
102+
103+
private static final Object sLock = new Object();
104+
105+
/**
106+
* Input stream for reading from Linux PRNG or {@code null} if not yet
107+
* opened.
108+
*
109+
* @GuardedBy("sLock")
110+
*/
111+
private static DataInputStream sUrandomIn;
112+
113+
/**
114+
* Output stream for writing to Linux PRNG or {@code null} if not yet
115+
* opened.
116+
*
117+
* @GuardedBy("sLock")
118+
*/
119+
private static OutputStream sUrandomOut;
120+
121+
/**
122+
* Whether this engine instance has been seeded. This is needed because
123+
* each instance needs to seed itself if the client does not explicitly
124+
* seed it.
125+
*/
126+
private boolean mSeeded;
127+
128+
@Override
129+
protected void engineSetSeed(byte[] bytes)
130+
{
131+
try {
132+
OutputStream out;
133+
synchronized (sLock) {
134+
out = getUrandomOutputStream();
135+
}
136+
out.write(bytes);
137+
out.flush();
138+
} catch (IOException e) {
139+
// On a small fraction of devices /dev/urandom is not writable.
140+
// Log and ignore.
141+
Log.w(LinuxPrngSecureRandomSpi.class.getSimpleName(),
142+
"Failed to mix seed into " + URANDOM_FILE);
143+
} finally {
144+
mSeeded = true;
145+
}
146+
}
147+
148+
@Override
149+
protected void engineNextBytes(byte[] bytes) {
150+
if (!mSeeded) {
151+
// Mix in the device- and invocation-specific seed.
152+
engineSetSeed(generateSeed());
153+
}
154+
155+
try {
156+
DataInputStream in;
157+
synchronized (sLock) {
158+
in = getUrandomInputStream();
159+
}
160+
synchronized (in) {
161+
in.readFully(bytes);
162+
}
163+
} catch (IOException e) {
164+
throw new SecurityException(
165+
"Failed to read from " + URANDOM_FILE, e);
166+
}
167+
}
168+
169+
@Override
170+
protected byte[] engineGenerateSeed(int size)
171+
{
172+
byte[] seed = new byte[size];
173+
engineNextBytes(seed);
174+
return seed;
175+
}
176+
177+
private DataInputStream getUrandomInputStream()
178+
{
179+
synchronized (sLock) {
180+
if (sUrandomIn == null) {
181+
// NOTE: Consider inserting a BufferedInputStream between
182+
// DataInputStream and FileInputStream if you need higher
183+
// PRNG output performance and can live with future PRNG
184+
// output being pulled into this process prematurely.
185+
try {
186+
sUrandomIn = new DataInputStream(
187+
new FileInputStream(URANDOM_FILE));
188+
} catch (IOException e) {
189+
throw new SecurityException("Failed to open "
190+
+ URANDOM_FILE + " for reading", e);
191+
}
192+
}
193+
return sUrandomIn;
194+
}
195+
}
196+
197+
private OutputStream getUrandomOutputStream() throws IOException
198+
{
199+
synchronized (sLock) {
200+
if (sUrandomOut == null) {
201+
sUrandomOut = new FileOutputStream(URANDOM_FILE);
202+
}
203+
return sUrandomOut;
204+
}
205+
}
206+
}
207+
208+
/**
209+
* Generates a device- and invocation-specific seed to be mixed into the
210+
* Linux PRNG.
211+
*/
212+
private static byte[] generateSeed()
213+
{
214+
try {
215+
ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
216+
DataOutputStream seedBufferOut =
217+
new DataOutputStream(seedBuffer);
218+
seedBufferOut.writeLong(System.currentTimeMillis());
219+
seedBufferOut.writeLong(System.nanoTime());
220+
seedBufferOut.writeInt(Process.myPid());
221+
seedBufferOut.writeInt(Process.myUid());
222+
seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
223+
seedBufferOut.close();
224+
return seedBuffer.toByteArray();
225+
} catch (IOException e) {
226+
throw new SecurityException("Failed to generate seed", e);
227+
}
228+
}
229+
230+
/**
231+
* Gets the hardware serial number of this device.
232+
*
233+
* @return serial number or {@code null} if not available.
234+
*/
235+
private static String getDeviceSerialNumber()
236+
{
237+
// We're using the Reflection API because Build.SERIAL is only available
238+
// since API Level 9 (Gingerbread, Android 2.3).
239+
try {
240+
return (String) Build.class.getField("SERIAL").get(null);
241+
} catch (Exception ignored) {
242+
return null;
243+
}
244+
}
245+
246+
private static byte[] getBuildFingerprintAndDeviceSerial()
247+
{
248+
StringBuilder result = new StringBuilder();
249+
String fingerprint = Build.FINGERPRINT;
250+
if (fingerprint != null) {
251+
result.append(fingerprint);
252+
}
253+
String serial = getDeviceSerialNumber();
254+
if (serial != null) {
255+
result.append(serial);
256+
}
257+
try {
258+
return result.toString().getBytes("UTF-8");
259+
} catch (UnsupportedEncodingException e) {
260+
throw new RuntimeException("UTF-8 encoding not supported");
261+
}
262+
}
263+
}

0 commit comments

Comments
 (0)