1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server;
18 
19 import android.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.IntentFilter;
23 import android.os.Binder;
24 import android.os.Environment;
25 import android.os.Handler;
26 import android.os.Message;
27 import android.os.SystemProperties;
28 import android.util.AtomicFile;
29 import android.util.Slog;
30 
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.internal.util.Preconditions;
33 
34 import java.io.File;
35 import java.io.FileInputStream;
36 import java.io.FileNotFoundException;
37 import java.io.FileOutputStream;
38 import java.io.IOException;
39 import java.nio.ByteBuffer;
40 import java.security.MessageDigest;
41 import java.security.NoSuchAlgorithmException;
42 
43 /**
44  * A service that loads and periodically saves "randomness" for the
45  * Linux kernel RNG.
46  *
47  * <p>When a Linux system starts up, the entropy pool associated with {@code
48  * /dev/urandom}, {@code /dev/random}, and {@code getrandom()} may be in a
49  * fairly predictable state, depending on the entropy sources available to the
50  * kernel.  Applications that depend on randomness may find these APIs returning
51  * predictable data.  To counteract this effect, this service maintains a seed
52  * file across shutdowns and startups, and also mixes some device and
53  * boot-specific information into the pool.
54  */
55 public class EntropyMixer extends Binder {
56     private static final String TAG = "EntropyMixer";
57     private static final int UPDATE_SEED_MSG = 1;
58     private static final int SEED_UPDATE_PERIOD = 3 * 60 * 60 * 1000;  // 3 hrs
59     private static final long START_TIME = System.currentTimeMillis();
60     private static final long START_NANOTIME = System.nanoTime();
61 
62     /*
63      * The size of the seed file in bytes.  This must be at least the size of a
64      * SHA-256 digest (32 bytes).  It *should* also be at least the size of the
65      * kernel's entropy pool (/proc/sys/kernel/random/poolsize divided by 8),
66      * which historically was 512 bytes, but changed to 32 bytes in Linux v5.18.
67      * There's actually no real need for more than a 32-byte seed, even with
68      * older kernels; however, we take the conservative approach of staying with
69      * the 512-byte size for now, as the cost is very small.
70      */
71     @VisibleForTesting
72     static final int SEED_FILE_SIZE = 512;
73 
74     @VisibleForTesting
75     static final String DEVICE_SPECIFIC_INFO_HEADER =
76         "Copyright (C) 2009 The Android Open Source Project\n" +
77         "All Your Randomness Are Belong To Us\n";
78 
79     private final AtomicFile seedFile;
80     private final File randomReadDevice;
81     private final File randomWriteDevice; // separate from randomReadDevice only for testing
82 
83     /**
84      * Handler that periodically updates the seed file.
85      */
86     private final Handler mHandler = new Handler(IoThread.getHandler().getLooper()) {
87         // IMPLEMENTATION NOTE: This handler runs on the I/O thread to avoid I/O on the main thread.
88         // The reason we're using our own Handler instead of IoThread.getHandler() is to create our
89         // own ID space for the "what" parameter of messages seen by the handler.
90         @Override
91         public void handleMessage(Message msg) {
92             if (msg.what != UPDATE_SEED_MSG) {
93                 Slog.e(TAG, "Will not process invalid message");
94                 return;
95             }
96             updateSeedFile();
97             scheduleSeedUpdater();
98         }
99     };
100 
101     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
102         @Override
103         public void onReceive(Context context, Intent intent) {
104             updateSeedFile();
105         }
106     };
107 
EntropyMixer(Context context)108     public EntropyMixer(Context context) {
109         this(context, new File(getSystemDir(), "entropy.dat"),
110                 new File("/dev/urandom"), new File("/dev/urandom"));
111     }
112 
113     @VisibleForTesting
EntropyMixer(Context context, File seedFile, File randomReadDevice, File randomWriteDevice)114     EntropyMixer(Context context, File seedFile, File randomReadDevice, File randomWriteDevice) {
115         this.seedFile = new AtomicFile(Preconditions.checkNotNull(seedFile));
116         this.randomReadDevice = Preconditions.checkNotNull(randomReadDevice);
117         this.randomWriteDevice = Preconditions.checkNotNull(randomWriteDevice);
118 
119         loadInitialEntropy();
120         updateSeedFile();
121         scheduleSeedUpdater();
122         IntentFilter broadcastFilter = new IntentFilter(Intent.ACTION_SHUTDOWN);
123         broadcastFilter.addAction(Intent.ACTION_POWER_CONNECTED);
124         broadcastFilter.addAction(Intent.ACTION_REBOOT);
125         context.registerReceiver(
126                 mBroadcastReceiver,
127                 broadcastFilter,
128                 null, // do not require broadcaster to hold any permissions
129                 mHandler // process received broadcasts on the I/O thread instead of the main thread
130                 );
131     }
132 
scheduleSeedUpdater()133     private void scheduleSeedUpdater() {
134         mHandler.removeMessages(UPDATE_SEED_MSG);
135         mHandler.sendEmptyMessageDelayed(UPDATE_SEED_MSG, SEED_UPDATE_PERIOD);
136     }
137 
loadInitialEntropy()138     private void loadInitialEntropy() {
139         byte[] seed = readSeedFile();
140         try (FileOutputStream out = new FileOutputStream(randomWriteDevice)) {
141             if (seed.length != 0) {
142                 out.write(seed);
143                 Slog.i(TAG, "Loaded existing seed file");
144             }
145             out.write(getDeviceSpecificInformation());
146         } catch (IOException e) {
147             Slog.e(TAG, "Error writing to " + randomWriteDevice, e);
148         }
149     }
150 
readSeedFile()151     private byte[] readSeedFile() {
152         try {
153             return seedFile.readFully();
154         } catch (FileNotFoundException e) {
155             return new byte[0];
156         } catch (IOException e) {
157             Slog.e(TAG, "Error reading " + seedFile.getBaseFile(), e);
158             return new byte[0];
159         }
160     }
161 
162     /**
163      * Update (or create) the seed file.
164      *
165      * <p>Traditionally, the recommended way to update a seed file on Linux was
166      * to simply copy some bytes from /dev/urandom.  However, that isn't
167      * actually a good way to do it, because writes to /dev/urandom aren't
168      * guaranteed to immediately affect reads from /dev/urandom.  This can cause
169      * the new seed file to contain less entropy than the old one!
170      *
171      * <p>Instead, we generate the new seed by hashing the old seed together
172      * with some bytes from /dev/urandom, following the example of <a
173      * href="https://git.zx2c4.com/seedrng/tree/README.md">SeedRNG</a>.  This
174      * ensures that the new seed is at least as entropic as the old seed.
175      */
updateSeedFile()176     private void updateSeedFile() {
177         byte[] oldSeed = readSeedFile();
178         byte[] newSeed = new byte[SEED_FILE_SIZE];
179 
180         try (FileInputStream in = new FileInputStream(randomReadDevice)) {
181             if (in.read(newSeed) != newSeed.length) {
182                 throw new IOException("unexpected EOF");
183             }
184         } catch (IOException e) {
185             Slog.e(TAG, "Error reading " + randomReadDevice +
186                     "; seed file won't be properly updated", e);
187             // Continue on; at least we'll have new timestamps...
188         }
189 
190         // newSeed = newSeed[:-32] ||
191         //           SHA-256(fixed_prefix || real_time || boot_time ||
192         //                   old_seed_len || old_seed || new_seed_len || new_seed)
193         MessageDigest sha256;
194         try {
195             sha256 = MessageDigest.getInstance("SHA-256");
196         } catch (NoSuchAlgorithmException e) {
197             Slog.wtf(TAG, "SHA-256 algorithm not found; seed file won't be updated", e);
198             return;
199         }
200         // This fixed prefix should be changed if the fields that are hashed change.
201         sha256.update("Android EntropyMixer v1".getBytes());
202         sha256.update(longToBytes(System.currentTimeMillis()));
203         sha256.update(longToBytes(System.nanoTime()));
204         sha256.update(longToBytes(oldSeed.length));
205         sha256.update(oldSeed);
206         sha256.update(longToBytes(newSeed.length));
207         sha256.update(newSeed);
208         byte[] digest = sha256.digest();
209         System.arraycopy(digest, 0, newSeed, newSeed.length - digest.length, digest.length);
210 
211         writeNewSeed(newSeed);
212         if (oldSeed.length == 0) {
213             Slog.i(TAG, "Created seed file");
214         } else {
215             Slog.i(TAG, "Updated seed file");
216         }
217     }
218 
writeNewSeed(byte[] newSeed)219     private void writeNewSeed(byte[] newSeed) {
220         FileOutputStream out = null;
221         try {
222             out = seedFile.startWrite();
223             out.write(newSeed);
224             seedFile.finishWrite(out);
225         } catch (IOException e) {
226             Slog.e(TAG, "Error writing " + seedFile.getBaseFile(), e);
227             seedFile.failWrite(out);
228         }
229     }
230 
longToBytes(long x)231     private static byte[] longToBytes(long x) {
232         ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
233         buffer.putLong(x);
234         return buffer.array();
235     }
236 
237     /**
238      * Get some device and boot-specific information to mix into the kernel's
239      * entropy pool.  This information probably won't contain much actual
240      * entropy, but that's fine because we don't ask the kernel to credit it.
241      * Writes to {@code /dev/urandom} can only increase or have no effect on the
242      * quality of random numbers, never decrease it.
243      *
244      * <p>The main goal here is just to initialize the entropy pool differently
245      * on devices that might otherwise be identical and have very little other
246      * entropy available.  Therefore, we include various system properties that
247      * can vary on a per-device and/or per-build basis.  We also include some
248      * timestamps, as these might vary on a per-boot basis and be not easily
249      * observable or guessable by an attacker.
250      */
getDeviceSpecificInformation()251     private byte[] getDeviceSpecificInformation() {
252         StringBuilder b = new StringBuilder();
253         b.append(DEVICE_SPECIFIC_INFO_HEADER);
254         b.append(START_TIME).append('\n');
255         b.append(START_NANOTIME).append('\n');
256         b.append(SystemProperties.get("ro.serialno")).append('\n');
257         b.append(SystemProperties.get("ro.bootmode")).append('\n');
258         b.append(SystemProperties.get("ro.baseband")).append('\n');
259         b.append(SystemProperties.get("ro.carrier")).append('\n');
260         b.append(SystemProperties.get("ro.bootloader")).append('\n');
261         b.append(SystemProperties.get("ro.hardware")).append('\n');
262         b.append(SystemProperties.get("ro.revision")).append('\n');
263         b.append(SystemProperties.get("ro.build.fingerprint")).append('\n');
264         b.append(new Object().hashCode()).append('\n');
265         b.append(System.currentTimeMillis()).append('\n');
266         b.append(System.nanoTime()).append('\n');
267         return b.toString().getBytes();
268     }
269 
getSystemDir()270     private static File getSystemDir() {
271         File dataDir = Environment.getDataDirectory();
272         File systemDir = new File(dataDir, "system");
273         systemDir.mkdirs();
274         return systemDir;
275     }
276 }
277