1/*
2 * Copyright (C) 2016 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
17package com.android.server;
18
19import android.content.Context;
20import android.net.LocalSocket;
21import android.net.LocalSocketAddress;
22import android.os.IRecoverySystem;
23import android.os.IRecoverySystemProgressListener;
24import android.os.PowerManager;
25import android.os.RecoverySystem;
26import android.os.RemoteException;
27import android.os.SystemProperties;
28import android.system.ErrnoException;
29import android.system.Os;
30import android.util.Slog;
31
32import libcore.io.IoUtils;
33
34import java.io.DataInputStream;
35import java.io.DataOutputStream;
36import java.io.File;
37import java.io.FileWriter;
38import java.io.IOException;
39
40/**
41 * The recovery system service is responsible for coordinating recovery related
42 * functions on the device. It sets up (or clears) the bootloader control block
43 * (BCB), which will be read by the bootloader and the recovery image. It also
44 * triggers /system/bin/uncrypt via init to de-encrypt an OTA package on the
45 * /data partition so that it can be accessed under the recovery image.
46 */
47public final class RecoverySystemService extends SystemService {
48    private static final String TAG = "RecoverySystemService";
49    private static final boolean DEBUG = false;
50
51    // The socket at /dev/socket/uncrypt to communicate with uncrypt.
52    private static final String UNCRYPT_SOCKET = "uncrypt";
53
54    // The init services that communicate with /system/bin/uncrypt.
55    private static final String INIT_SERVICE_UNCRYPT = "init.svc.uncrypt";
56    private static final String INIT_SERVICE_SETUP_BCB = "init.svc.setup-bcb";
57    private static final String INIT_SERVICE_CLEAR_BCB = "init.svc.clear-bcb";
58
59    private static final int SOCKET_CONNECTION_MAX_RETRY = 30;
60
61    private static final Object sRequestLock = new Object();
62
63    private Context mContext;
64
65    public RecoverySystemService(Context context) {
66        super(context);
67        mContext = context;
68    }
69
70    @Override
71    public void onStart() {
72        publishBinderService(Context.RECOVERY_SERVICE, new BinderService());
73    }
74
75    private final class BinderService extends IRecoverySystem.Stub {
76        @Override // Binder call
77        public boolean uncrypt(String filename, IRecoverySystemProgressListener listener) {
78            if (DEBUG) Slog.d(TAG, "uncrypt: " + filename);
79
80            synchronized (sRequestLock) {
81                mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
82
83                final boolean available = checkAndWaitForUncryptService();
84                if (!available) {
85                    Slog.e(TAG, "uncrypt service is unavailable.");
86                    return false;
87                }
88
89                // Write the filename into UNCRYPT_PACKAGE_FILE to be read by
90                // uncrypt.
91                RecoverySystem.UNCRYPT_PACKAGE_FILE.delete();
92
93                try (FileWriter uncryptFile = new FileWriter(RecoverySystem.UNCRYPT_PACKAGE_FILE)) {
94                    uncryptFile.write(filename + "\n");
95                } catch (IOException e) {
96                    Slog.e(TAG, "IOException when writing \"" +
97                            RecoverySystem.UNCRYPT_PACKAGE_FILE + "\":", e);
98                    return false;
99                }
100
101                // Trigger uncrypt via init.
102                SystemProperties.set("ctl.start", "uncrypt");
103
104                // Connect to the uncrypt service socket.
105                LocalSocket socket = connectService();
106                if (socket == null) {
107                    Slog.e(TAG, "Failed to connect to uncrypt socket");
108                    return false;
109                }
110
111                // Read the status from the socket.
112                DataInputStream dis = null;
113                DataOutputStream dos = null;
114                try {
115                    dis = new DataInputStream(socket.getInputStream());
116                    dos = new DataOutputStream(socket.getOutputStream());
117                    int lastStatus = Integer.MIN_VALUE;
118                    while (true) {
119                        int status = dis.readInt();
120                        // Avoid flooding the log with the same message.
121                        if (status == lastStatus && lastStatus != Integer.MIN_VALUE) {
122                            continue;
123                        }
124                        lastStatus = status;
125
126                        if (status >= 0 && status <= 100) {
127                            // Update status
128                            Slog.i(TAG, "uncrypt read status: " + status);
129                            if (listener != null) {
130                                try {
131                                    listener.onProgress(status);
132                                } catch (RemoteException ignored) {
133                                    Slog.w(TAG, "RemoteException when posting progress");
134                                }
135                            }
136                            if (status == 100) {
137                                Slog.i(TAG, "uncrypt successfully finished.");
138                                // Ack receipt of the final status code. uncrypt
139                                // waits for the ack so the socket won't be
140                                // destroyed before we receive the code.
141                                dos.writeInt(0);
142                                break;
143                            }
144                        } else {
145                            // Error in /system/bin/uncrypt.
146                            Slog.e(TAG, "uncrypt failed with status: " + status);
147                            // Ack receipt of the final status code. uncrypt waits
148                            // for the ack so the socket won't be destroyed before
149                            // we receive the code.
150                            dos.writeInt(0);
151                            return false;
152                        }
153                    }
154                } catch (IOException e) {
155                    Slog.e(TAG, "IOException when reading status: ", e);
156                    return false;
157                } finally {
158                    IoUtils.closeQuietly(dis);
159                    IoUtils.closeQuietly(dos);
160                    IoUtils.closeQuietly(socket);
161                }
162
163                return true;
164            }
165        }
166
167        @Override // Binder call
168        public boolean clearBcb() {
169            if (DEBUG) Slog.d(TAG, "clearBcb");
170            synchronized (sRequestLock) {
171                return setupOrClearBcb(false, null);
172            }
173        }
174
175        @Override // Binder call
176        public boolean setupBcb(String command) {
177            if (DEBUG) Slog.d(TAG, "setupBcb: [" + command + "]");
178            synchronized (sRequestLock) {
179                return setupOrClearBcb(true, command);
180            }
181        }
182
183        @Override // Binder call
184        public void rebootRecoveryWithCommand(String command) {
185            if (DEBUG) Slog.d(TAG, "rebootRecoveryWithCommand: [" + command + "]");
186            synchronized (sRequestLock) {
187                if (!setupOrClearBcb(true, command)) {
188                    return;
189                }
190
191                // Having set up the BCB, go ahead and reboot.
192                PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
193                pm.reboot(PowerManager.REBOOT_RECOVERY);
194            }
195        }
196
197        /**
198         * Check if any of the init services is still running. If so, we cannot
199         * start a new uncrypt/setup-bcb/clear-bcb service right away; otherwise
200         * it may break the socket communication since init creates / deletes
201         * the socket (/dev/socket/uncrypt) on service start / exit.
202         */
203        private boolean checkAndWaitForUncryptService() {
204            for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
205                final String uncryptService = SystemProperties.get(INIT_SERVICE_UNCRYPT);
206                final String setupBcbService = SystemProperties.get(INIT_SERVICE_SETUP_BCB);
207                final String clearBcbService = SystemProperties.get(INIT_SERVICE_CLEAR_BCB);
208                final boolean busy = "running".equals(uncryptService) ||
209                        "running".equals(setupBcbService) || "running".equals(clearBcbService);
210                if (DEBUG) {
211                    Slog.i(TAG, "retry: " + retry + " busy: " + busy +
212                            " uncrypt: [" + uncryptService + "]" +
213                            " setupBcb: [" + setupBcbService + "]" +
214                            " clearBcb: [" + clearBcbService + "]");
215                }
216
217                if (!busy) {
218                    return true;
219                }
220
221                try {
222                    Thread.sleep(1000);
223                } catch (InterruptedException e) {
224                    Slog.w(TAG, "Interrupted:", e);
225                }
226            }
227
228            return false;
229        }
230
231        private LocalSocket connectService() {
232            LocalSocket socket = new LocalSocket();
233            boolean done = false;
234            // The uncrypt socket will be created by init upon receiving the
235            // service request. It may not be ready by this point. So we will
236            // keep retrying until success or reaching timeout.
237            for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
238                try {
239                    socket.connect(new LocalSocketAddress(UNCRYPT_SOCKET,
240                            LocalSocketAddress.Namespace.RESERVED));
241                    done = true;
242                    break;
243                } catch (IOException ignored) {
244                    try {
245                        Thread.sleep(1000);
246                    } catch (InterruptedException e) {
247                        Slog.w(TAG, "Interrupted:", e);
248                    }
249                }
250            }
251            if (!done) {
252                Slog.e(TAG, "Timed out connecting to uncrypt socket");
253                return null;
254            }
255            return socket;
256        }
257
258        private boolean setupOrClearBcb(boolean isSetup, String command) {
259            mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
260
261            final boolean available = checkAndWaitForUncryptService();
262            if (!available) {
263                Slog.e(TAG, "uncrypt service is unavailable.");
264                return false;
265            }
266
267            if (isSetup) {
268                SystemProperties.set("ctl.start", "setup-bcb");
269            } else {
270                SystemProperties.set("ctl.start", "clear-bcb");
271            }
272
273            // Connect to the uncrypt service socket.
274            LocalSocket socket = connectService();
275            if (socket == null) {
276                Slog.e(TAG, "Failed to connect to uncrypt socket");
277                return false;
278            }
279
280            DataInputStream dis = null;
281            DataOutputStream dos = null;
282            try {
283                dis = new DataInputStream(socket.getInputStream());
284                dos = new DataOutputStream(socket.getOutputStream());
285
286                // Send the BCB commands if it's to setup BCB.
287                if (isSetup) {
288                    dos.writeInt(command.length());
289                    dos.writeBytes(command);
290                    dos.flush();
291                }
292
293                // Read the status from the socket.
294                int status = dis.readInt();
295
296                // Ack receipt of the status code. uncrypt waits for the ack so
297                // the socket won't be destroyed before we receive the code.
298                dos.writeInt(0);
299
300                if (status == 100) {
301                    Slog.i(TAG, "uncrypt " + (isSetup ? "setup" : "clear") +
302                            " bcb successfully finished.");
303                } else {
304                    // Error in /system/bin/uncrypt.
305                    Slog.e(TAG, "uncrypt failed with status: " + status);
306                    return false;
307                }
308            } catch (IOException e) {
309                Slog.e(TAG, "IOException when communicating with uncrypt:", e);
310                return false;
311            } finally {
312                IoUtils.closeQuietly(dis);
313                IoUtils.closeQuietly(dos);
314                IoUtils.closeQuietly(socket);
315            }
316
317            return true;
318        }
319    }
320}
321