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.RecoverySystem;
25import android.os.RemoteException;
26import android.os.SystemProperties;
27import android.system.ErrnoException;
28import android.system.Os;
29import android.util.Slog;
30
31import libcore.io.IoUtils;
32
33import java.io.DataInputStream;
34import java.io.DataOutputStream;
35import java.io.File;
36import java.io.FileWriter;
37import java.io.IOException;
38
39/**
40 * The recovery system service is responsible for coordinating recovery related
41 * functions on the device. It sets up (or clears) the bootloader control block
42 * (BCB), which will be read by the bootloader and the recovery image. It also
43 * triggers /system/bin/uncrypt via init to de-encrypt an OTA package on the
44 * /data partition so that it can be accessed under the recovery image.
45 */
46public final class RecoverySystemService extends SystemService {
47    private static final String TAG = "RecoverySystemService";
48    private static final boolean DEBUG = false;
49
50    // The socket at /dev/socket/uncrypt to communicate with uncrypt.
51    private static final String UNCRYPT_SOCKET = "uncrypt";
52
53    private static final int SOCKET_CONNECTION_MAX_RETRY = 30;
54
55    private Context mContext;
56
57    public RecoverySystemService(Context context) {
58        super(context);
59        mContext = context;
60    }
61
62    @Override
63    public void onStart() {
64        publishBinderService(Context.RECOVERY_SERVICE, new BinderService());
65    }
66
67    private final class BinderService extends IRecoverySystem.Stub {
68        @Override // Binder call
69        public boolean uncrypt(String filename, IRecoverySystemProgressListener listener) {
70            if (DEBUG) Slog.d(TAG, "uncrypt: " + filename);
71
72            mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
73
74            // Write the filename into UNCRYPT_PACKAGE_FILE to be read by
75            // uncrypt.
76            RecoverySystem.UNCRYPT_PACKAGE_FILE.delete();
77
78            try (FileWriter uncryptFile = new FileWriter(RecoverySystem.UNCRYPT_PACKAGE_FILE)) {
79                uncryptFile.write(filename + "\n");
80            } catch (IOException e) {
81                Slog.e(TAG, "IOException when writing \"" + RecoverySystem.UNCRYPT_PACKAGE_FILE +
82                        "\": ", e);
83                return false;
84            }
85
86            // Trigger uncrypt via init.
87            SystemProperties.set("ctl.start", "uncrypt");
88
89            // Connect to the uncrypt service socket.
90            LocalSocket socket = connectService();
91            if (socket == null) {
92                Slog.e(TAG, "Failed to connect to uncrypt socket");
93                return false;
94            }
95
96            // Read the status from the socket.
97            DataInputStream dis = null;
98            DataOutputStream dos = null;
99            try {
100                dis = new DataInputStream(socket.getInputStream());
101                dos = new DataOutputStream(socket.getOutputStream());
102                int lastStatus = Integer.MIN_VALUE;
103                while (true) {
104                    int status = dis.readInt();
105                    // Avoid flooding the log with the same message.
106                    if (status == lastStatus && lastStatus != Integer.MIN_VALUE) {
107                        continue;
108                    }
109                    lastStatus = status;
110
111                    if (status >= 0 && status <= 100) {
112                        // Update status
113                        Slog.i(TAG, "uncrypt read status: " + status);
114                        if (listener != null) {
115                            try {
116                                listener.onProgress(status);
117                            } catch (RemoteException ignored) {
118                                Slog.w(TAG, "RemoteException when posting progress");
119                            }
120                        }
121                        if (status == 100) {
122                            Slog.i(TAG, "uncrypt successfully finished.");
123                            // Ack receipt of the final status code. uncrypt
124                            // waits for the ack so the socket won't be
125                            // destroyed before we receive the code.
126                            dos.writeInt(0);
127                            break;
128                        }
129                    } else {
130                        // Error in /system/bin/uncrypt.
131                        Slog.e(TAG, "uncrypt failed with status: " + status);
132                        // Ack receipt of the final status code. uncrypt waits
133                        // for the ack so the socket won't be destroyed before
134                        // we receive the code.
135                        dos.writeInt(0);
136                        return false;
137                    }
138                }
139            } catch (IOException e) {
140                Slog.e(TAG, "IOException when reading status: ", e);
141                return false;
142            } finally {
143                IoUtils.closeQuietly(dis);
144                IoUtils.closeQuietly(dos);
145                IoUtils.closeQuietly(socket);
146            }
147
148            return true;
149        }
150
151        @Override // Binder call
152        public boolean clearBcb() {
153            if (DEBUG) Slog.d(TAG, "clearBcb");
154            return setupOrClearBcb(false, null);
155        }
156
157        @Override // Binder call
158        public boolean setupBcb(String command) {
159            if (DEBUG) Slog.d(TAG, "setupBcb: [" + command + "]");
160            return setupOrClearBcb(true, command);
161        }
162
163        private LocalSocket connectService() {
164            LocalSocket socket = new LocalSocket();
165            boolean done = false;
166            // The uncrypt socket will be created by init upon receiving the
167            // service request. It may not be ready by this point. So we will
168            // keep retrying until success or reaching timeout.
169            for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
170                try {
171                    socket.connect(new LocalSocketAddress(UNCRYPT_SOCKET,
172                            LocalSocketAddress.Namespace.RESERVED));
173                    done = true;
174                    break;
175                } catch (IOException ignored) {
176                    try {
177                        Thread.sleep(1000);
178                    } catch (InterruptedException e) {
179                        Slog.w(TAG, "Interrupted: ", e);
180                    }
181                }
182            }
183            if (!done) {
184                Slog.e(TAG, "Timed out connecting to uncrypt socket");
185                return null;
186            }
187            return socket;
188        }
189
190        private boolean setupOrClearBcb(boolean isSetup, String command) {
191            mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
192
193            if (isSetup) {
194                SystemProperties.set("ctl.start", "setup-bcb");
195            } else {
196                SystemProperties.set("ctl.start", "clear-bcb");
197            }
198
199            // Connect to the uncrypt service socket.
200            LocalSocket socket = connectService();
201            if (socket == null) {
202                Slog.e(TAG, "Failed to connect to uncrypt socket");
203                return false;
204            }
205
206            DataInputStream dis = null;
207            DataOutputStream dos = null;
208            try {
209                dis = new DataInputStream(socket.getInputStream());
210                dos = new DataOutputStream(socket.getOutputStream());
211
212                // Send the BCB commands if it's to setup BCB.
213                if (isSetup) {
214                    dos.writeInt(command.length());
215                    dos.writeBytes(command);
216                    dos.flush();
217                }
218
219                // Read the status from the socket.
220                int status = dis.readInt();
221
222                // Ack receipt of the status code. uncrypt waits for the ack so
223                // the socket won't be destroyed before we receive the code.
224                dos.writeInt(0);
225
226                if (status == 100) {
227                    Slog.i(TAG, "uncrypt " + (isSetup ? "setup" : "clear") +
228                            " bcb successfully finished.");
229                } else {
230                    // Error in /system/bin/uncrypt.
231                    Slog.e(TAG, "uncrypt failed with status: " + status);
232                    return false;
233                }
234            } catch (IOException e) {
235                Slog.e(TAG, "IOException when communicating with uncrypt: ", e);
236                return false;
237            } finally {
238                IoUtils.closeQuietly(dis);
239                IoUtils.closeQuietly(dos);
240                IoUtils.closeQuietly(socket);
241            }
242
243            return true;
244        }
245    }
246}
247