1 /*
2  * Copyright (C) 2019 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.pm;
18 
19 import android.annotation.NonNull;
20 import android.content.ComponentName;
21 import android.content.pm.ArchivedPackageParcel;
22 import android.content.pm.DataLoaderParams;
23 import android.content.pm.InstallationFile;
24 import android.content.pm.PackageInstaller;
25 import android.os.Parcel;
26 import android.os.ParcelFileDescriptor;
27 import android.os.ShellCommand;
28 import android.service.dataloader.DataLoaderService;
29 import android.util.Slog;
30 import android.util.SparseArray;
31 
32 import com.android.internal.annotations.VisibleForTesting;
33 
34 import libcore.io.IoUtils;
35 
36 import java.io.IOException;
37 import java.lang.ref.WeakReference;
38 import java.nio.ByteBuffer;
39 import java.nio.ByteOrder;
40 import java.nio.charset.StandardCharsets;
41 import java.security.SecureRandom;
42 import java.util.Arrays;
43 import java.util.Collection;
44 import java.util.concurrent.atomic.AtomicLong;
45 
46 /**
47  * Callback data loader for PackageManagerShellCommand installations.
48  */
49 public class PackageManagerShellCommandDataLoader extends DataLoaderService {
50     public static final String TAG = "PackageManagerShellCommandDataLoader";
51 
52     private static final String PACKAGE = "android";
53     private static final String CLASS = PackageManagerShellCommandDataLoader.class.getName();
54 
55     static final SecureRandom sRandom = new SecureRandom();
56     static final SparseArray<WeakReference<ShellCommand>> sShellCommands = new SparseArray<>();
57 
58     private static final char ARGS_DELIM = '&';
59     private static final String SHELL_COMMAND_ID_PREFIX = "shellCommandId=";
60     private static final int INVALID_SHELL_COMMAND_ID = -1;
61     private static final int TOO_MANY_PENDING_SHELL_COMMANDS = 10;
62 
63     private static final String STDIN_PATH = "-";
64 
getDataLoaderParamsArgs(ShellCommand shellCommand)65     private static String getDataLoaderParamsArgs(ShellCommand shellCommand) {
66         nativeInitialize();
67 
68         int commandId;
69         synchronized (sShellCommands) {
70             // Clean up old references.
71             for (int i = sShellCommands.size() - 1; i >= 0; i--) {
72                 WeakReference<ShellCommand> oldRef = sShellCommands.valueAt(i);
73                 if (oldRef.get() == null) {
74                     sShellCommands.removeAt(i);
75                 }
76             }
77 
78             if (sShellCommands.size() > TOO_MANY_PENDING_SHELL_COMMANDS) {
79                 Slog.e(TAG, "Too many pending shell commands: " + sShellCommands.size());
80             }
81 
82             // Generate new id and put ref to the array.
83             do {
84                 commandId = sRandom.nextInt(Integer.MAX_VALUE - 1) + 1;
85             } while (sShellCommands.contains(commandId));
86 
87             sShellCommands.put(commandId, new WeakReference<>(shellCommand));
88         }
89 
90         return SHELL_COMMAND_ID_PREFIX + commandId;
91     }
92 
getStreamingDataLoaderParams(ShellCommand shellCommand)93     static DataLoaderParams getStreamingDataLoaderParams(ShellCommand shellCommand) {
94         return DataLoaderParams.forStreaming(new ComponentName(PACKAGE, CLASS),
95                 getDataLoaderParamsArgs(shellCommand));
96     }
97 
getIncrementalDataLoaderParams(ShellCommand shellCommand)98     static DataLoaderParams getIncrementalDataLoaderParams(ShellCommand shellCommand) {
99         return DataLoaderParams.forIncremental(new ComponentName(PACKAGE, CLASS),
100                 getDataLoaderParamsArgs(shellCommand));
101     }
102 
extractShellCommandId(String args)103     private static int extractShellCommandId(String args) {
104         int sessionIdIdx = args.indexOf(SHELL_COMMAND_ID_PREFIX);
105         if (sessionIdIdx < 0) {
106             Slog.e(TAG, "Missing shell command id param.");
107             return INVALID_SHELL_COMMAND_ID;
108         }
109         sessionIdIdx += SHELL_COMMAND_ID_PREFIX.length();
110         int delimIdx = args.indexOf(ARGS_DELIM, sessionIdIdx);
111         try {
112             if (delimIdx < 0) {
113                 return Integer.parseInt(args.substring(sessionIdIdx));
114             } else {
115                 return Integer.parseInt(args.substring(sessionIdIdx, delimIdx));
116             }
117         } catch (NumberFormatException e) {
118             Slog.e(TAG, "Incorrect shell command id format.", e);
119             return INVALID_SHELL_COMMAND_ID;
120         }
121     }
122 
123     /** @hide */
124     @VisibleForTesting
125     public static class Metadata {
126         /**
127          * Full files read from stdin.
128          */
129         static final byte STDIN = 0;
130         /**
131          * Full files read from local file.
132          */
133         static final byte LOCAL_FILE = 1;
134         /**
135          * Signature tree read from stdin, data streamed.
136          */
137         static final byte DATA_ONLY_STREAMING = 2;
138         /**
139          * Everything streamed.
140          */
141         static final byte STREAMING = 3;
142         /**
143          * Archived install.
144          */
145         static final byte ARCHIVED = 4;
146 
147         private final byte mMode;
148         private final byte[] mData;
149         private final String mSalt;
150 
151         private static final AtomicLong sGlobalSalt =
152                 new AtomicLong((new SecureRandom()).nextLong());
nextGlobalSalt()153         private static Long nextGlobalSalt() {
154             return sGlobalSalt.incrementAndGet();
155         }
156 
forStdIn(String fileId)157         static Metadata forStdIn(String fileId) {
158             return new Metadata(STDIN, fileId);
159         }
160 
161         /** @hide */
162         @VisibleForTesting
forLocalFile(String filePath)163         public static Metadata forLocalFile(String filePath) {
164             return new Metadata(LOCAL_FILE, filePath, nextGlobalSalt().toString());
165         }
166 
167         /** @hide */
168         @VisibleForTesting
forArchived(ArchivedPackageParcel archivedPackage)169         public static Metadata forArchived(ArchivedPackageParcel archivedPackage) {
170             return new Metadata(ARCHIVED, writeArchivedPackageParcel(archivedPackage), null);
171         }
172 
forDataOnlyStreaming(String fileId)173         static Metadata forDataOnlyStreaming(String fileId) {
174             return new Metadata(DATA_ONLY_STREAMING, fileId);
175         }
176 
forStreaming(String fileId)177         static Metadata forStreaming(String fileId) {
178             return new Metadata(STREAMING, fileId);
179         }
180 
Metadata(byte mode, String data)181         private Metadata(byte mode, String data) {
182             this(mode, data, null);
183         }
184 
Metadata(byte mode, String data, String salt)185         private Metadata(byte mode, String data, String salt) {
186             this(mode, (data != null ? data : "").getBytes(StandardCharsets.UTF_8), salt);
187         }
188 
Metadata(byte mode, byte[] data, String salt)189         private Metadata(byte mode, byte[] data, String salt) {
190             this.mMode = mode;
191             this.mData = data;
192             this.mSalt = salt;
193         }
194 
fromByteArray(byte[] bytes)195         static Metadata fromByteArray(byte[] bytes) throws IOException {
196             if (bytes == null || bytes.length < 5) {
197                 return null;
198             }
199             int offset = 0;
200             final byte mode = bytes[offset];
201             offset += 1;
202             final byte[] data;
203             final String salt;
204             switch (mode) {
205                 case LOCAL_FILE: {
206                     int dataSize = ByteBuffer.wrap(bytes, offset, 4).order(
207                             ByteOrder.LITTLE_ENDIAN).getInt();
208                     offset += 4;
209                     data = Arrays.copyOfRange(bytes, offset, offset + dataSize);
210                     offset += dataSize;
211                     salt = new String(bytes, offset, bytes.length - offset,
212                             StandardCharsets.UTF_8);
213                     break;
214                 }
215                 default:
216                     data = Arrays.copyOfRange(bytes, offset, bytes.length);
217                     salt = null;
218                     break;
219             }
220             return new Metadata(mode, data, salt);
221         }
222 
223         /** @hide */
224         @VisibleForTesting
toByteArray()225         public byte[] toByteArray() {
226             final byte[] result;
227             final byte[] dataBytes = this.mData;
228             switch (this.mMode) {
229                 case LOCAL_FILE: {
230                     int dataSize = dataBytes.length;
231                     byte[] saltBytes = this.mSalt.getBytes(StandardCharsets.UTF_8);
232                     result = new byte[1 + 4 + dataSize + saltBytes.length];
233                     int offset = 0;
234                     result[offset] = this.mMode;
235                     offset += 1;
236                     ByteBuffer.wrap(result, offset, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(
237                             dataSize);
238                     offset += 4;
239                     System.arraycopy(dataBytes, 0, result, offset, dataSize);
240                     offset += dataSize;
241                     System.arraycopy(saltBytes, 0, result, offset, saltBytes.length);
242                     break;
243                 }
244                 default:
245                     result = new byte[1 + dataBytes.length];
246                     result[0] = this.mMode;
247                     System.arraycopy(dataBytes, 0, result, 1, dataBytes.length);
248                     break;
249             }
250             return result;
251         }
252 
getMode()253         byte getMode() {
254             return this.mMode;
255         }
256 
getData()257         byte[] getData() {
258             return this.mData;
259         }
260 
getArchivedPackage()261         ArchivedPackageParcel getArchivedPackage() {
262             if (getMode() != ARCHIVED) {
263                 throw new IllegalStateException("Not an archived package metadata.");
264             }
265             return readArchivedPackageParcel(this.mData);
266         }
267 
readArchivedPackageParcel(byte[] bytes)268         static ArchivedPackageParcel readArchivedPackageParcel(byte[] bytes) {
269             Parcel parcel = Parcel.obtain();
270             ArchivedPackageParcel result;
271             try {
272                 parcel.unmarshall(bytes, 0, bytes.length);
273                 parcel.setDataPosition(0);
274                 result = parcel.readParcelable(ArchivedPackageParcel.class.getClassLoader());
275             } finally {
276                 parcel.recycle();
277             }
278             return result;
279         }
280 
writeArchivedPackageParcel(ArchivedPackageParcel archivedPackage)281         static byte[] writeArchivedPackageParcel(ArchivedPackageParcel archivedPackage) {
282             Parcel parcel = Parcel.obtain();
283             try {
284                 parcel.writeParcelable(archivedPackage, 0);
285                 return parcel.marshall();
286             } finally {
287                 parcel.recycle();
288             }
289         }
290     }
291 
292     private static class DataLoader implements DataLoaderService.DataLoader {
293         private DataLoaderParams mParams = null;
294         private FileSystemConnector mConnector = null;
295 
296         @Override
onCreate(@onNull DataLoaderParams dataLoaderParams, @NonNull FileSystemConnector connector)297         public boolean onCreate(@NonNull DataLoaderParams dataLoaderParams,
298                 @NonNull FileSystemConnector connector) {
299             mParams = dataLoaderParams;
300             mConnector = connector;
301             return true;
302         }
303 
304         @Override
onPrepareImage(@onNull Collection<InstallationFile> addedFiles, @NonNull Collection<String> removedFiles)305         public boolean onPrepareImage(@NonNull Collection<InstallationFile> addedFiles,
306                 @NonNull Collection<String> removedFiles) {
307             ShellCommand shellCommand = lookupShellCommand(mParams.getArguments());
308             try {
309                 for (InstallationFile file : addedFiles) {
310                     Metadata metadata = Metadata.fromByteArray(file.getMetadata());
311                     if (metadata == null) {
312                         Slog.e(TAG, "Invalid metadata for file: " + file.getName());
313                         return false;
314                     }
315                     switch (metadata.getMode()) {
316                         case Metadata.STDIN: {
317                             if (shellCommand == null) {
318                                 Slog.e(TAG, "Missing shell command for Metadata.STDIN.");
319                                 return false;
320                             }
321                             final ParcelFileDescriptor inFd = getStdInPFD(shellCommand);
322                             mConnector.writeData(file.getName(), 0, file.getLengthBytes(), inFd);
323                             break;
324                         }
325                         case Metadata.LOCAL_FILE: {
326                             if (shellCommand == null) {
327                                 Slog.e(TAG, "Missing shell command for Metadata.LOCAL_FILE.");
328                                 return false;
329                             }
330                             ParcelFileDescriptor incomingFd = null;
331                             try {
332                                 final String filePath = new String(metadata.getData(),
333                                         StandardCharsets.UTF_8);
334                                 incomingFd = getLocalFilePFD(shellCommand, filePath);
335                                 mConnector.writeData(file.getName(), 0, incomingFd.getStatSize(),
336                                         incomingFd);
337                             } finally {
338                                 IoUtils.closeQuietly(incomingFd);
339                             }
340                             break;
341                         }
342                         case Metadata.ARCHIVED: {
343                             // Do nothing, metadata already contains everything needed for install.
344                             break;
345                         }
346                         default:
347                             Slog.e(TAG, "Unsupported metadata mode: " + metadata.getMode());
348                             return false;
349                     }
350                 }
351                 return true;
352             } catch (IOException e) {
353                 Slog.e(TAG, "Exception while streaming files", e);
354                 return false;
355             }
356         }
357     }
358 
lookupShellCommand(String args)359     static ShellCommand lookupShellCommand(String args) {
360         final int commandId = extractShellCommandId(args);
361         if (commandId == INVALID_SHELL_COMMAND_ID) {
362             return null;
363         }
364 
365         final WeakReference<ShellCommand> shellCommandRef;
366         synchronized (sShellCommands) {
367             shellCommandRef = sShellCommands.get(commandId, null);
368         }
369         final ShellCommand shellCommand =
370                 shellCommandRef != null ? shellCommandRef.get() : null;
371 
372         return shellCommand;
373     }
374 
getStdInPFD(ShellCommand shellCommand)375     static ParcelFileDescriptor getStdInPFD(ShellCommand shellCommand) {
376         try {
377             return ParcelFileDescriptor.dup(shellCommand.getInFileDescriptor());
378         } catch (IOException e) {
379             Slog.e(TAG, "Exception while obtaining STDIN fd", e);
380             return null;
381         }
382     }
383 
getLocalFilePFD(ShellCommand shellCommand, String filePath)384     static ParcelFileDescriptor getLocalFilePFD(ShellCommand shellCommand, String filePath) {
385         return shellCommand.openFileForSystem(filePath, "r");
386     }
387 
getStdIn(ShellCommand shellCommand)388     static int getStdIn(ShellCommand shellCommand) {
389         ParcelFileDescriptor pfd = getStdInPFD(shellCommand);
390         return pfd == null ? -1 : pfd.detachFd();
391     }
392 
getLocalFile(ShellCommand shellCommand, String filePath)393     static int getLocalFile(ShellCommand shellCommand, String filePath) {
394         ParcelFileDescriptor pfd = getLocalFilePFD(shellCommand, filePath);
395         return pfd == null ? -1 : pfd.detachFd();
396     }
397 
398     @Override
onCreateDataLoader( @onNull DataLoaderParams dataLoaderParams)399     public DataLoaderService.DataLoader onCreateDataLoader(
400             @NonNull DataLoaderParams dataLoaderParams) {
401         if (dataLoaderParams.getType() == PackageInstaller.DATA_LOADER_TYPE_STREAMING) {
402             // This DataLoader only supports streaming installations.
403             return new DataLoader();
404         }
405         return null;
406     }
407 
408     /* Native methods */
nativeInitialize()409     private static native void nativeInitialize();
410 }
411