1 /*
2  * Copyright (C) 2012 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.updates;
18 
19 import android.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.net.Uri;
23 import android.os.Binder;
24 import android.util.EventLog;
25 import android.util.Slog;
26 
27 import com.android.internal.util.HexDump;
28 import com.android.server.EventLogTags;
29 
30 import libcore.io.IoUtils;
31 import libcore.io.Streams;
32 
33 import java.io.BufferedInputStream;
34 import java.io.ByteArrayInputStream;
35 import java.io.File;
36 import java.io.FileOutputStream;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.security.MessageDigest;
40 import java.security.NoSuchAlgorithmException;
41 
42 public class ConfigUpdateInstallReceiver extends BroadcastReceiver {
43 
44     private static final String TAG = "ConfigUpdateInstallReceiver";
45 
46     private static final String EXTRA_REQUIRED_HASH = "REQUIRED_HASH";
47     private static final String EXTRA_VERSION_NUMBER = "VERSION";
48 
49     protected final File updateDir;
50     protected final File updateContent;
51     protected final File updateVersion;
52 
ConfigUpdateInstallReceiver(String updateDir, String updateContentPath, String updateMetadataPath, String updateVersionPath)53     public ConfigUpdateInstallReceiver(String updateDir, String updateContentPath,
54                                        String updateMetadataPath, String updateVersionPath) {
55         this.updateDir = new File(updateDir);
56         this.updateContent = new File(updateDir, updateContentPath);
57         File updateMetadataDir = new File(updateDir, updateMetadataPath);
58         this.updateVersion = new File(updateMetadataDir, updateVersionPath);
59     }
60 
61     @Override
onReceive(final Context context, final Intent intent)62     public void onReceive(final Context context, final Intent intent) {
63         new Thread() {
64             @Override
65             public void run() {
66                 try {
67                     // get the version from the extras
68                     int altVersion = getVersionFromIntent(intent);
69                     // get the previous value from the extras
70                     String altRequiredHash = getRequiredHashFromIntent(intent);
71                     // get the version currently being used
72                     int currentVersion = getCurrentVersion();
73                     // get the hash of the currently used value
74                     String currentHash = getCurrentHash(getCurrentContent());
75                     if (!verifyVersion(currentVersion, altVersion)) {
76                         Slog.i(TAG, "Not installing, new version is <= current version");
77                     } else if (!verifyPreviousHash(currentHash, altRequiredHash)) {
78                         EventLog.writeEvent(EventLogTags.CONFIG_INSTALL_FAILED,
79                                 "Current hash did not match required value");
80                     } else {
81                         // install the new content
82                         Slog.i(TAG, "Found new update, installing...");
83                         try (BufferedInputStream altContent = getAltContent(context, intent)) {
84                             install(altContent, altVersion);
85                         }
86                         Slog.i(TAG, "Installation successful");
87                         postInstall(context, intent);
88                     }
89                 } catch (Exception e) {
90                     Slog.e(TAG, "Could not update content!", e);
91                     // keep the error message <= 100 chars
92                     String errMsg = e.toString();
93                     if (errMsg.length() > 100) {
94                         errMsg = errMsg.substring(0, 99);
95                     }
96                     EventLog.writeEvent(EventLogTags.CONFIG_INSTALL_FAILED, errMsg);
97                 }
98             }
99         }.start();
100     }
101 
getContentFromIntent(Intent i)102     private Uri getContentFromIntent(Intent i) {
103         Uri data = i.getData();
104         if (data == null) {
105             throw new IllegalStateException("Missing required content path, ignoring.");
106         }
107         return data;
108     }
109 
getVersionFromIntent(Intent i)110     private int getVersionFromIntent(Intent i) throws NumberFormatException {
111         String extraValue = i.getStringExtra(EXTRA_VERSION_NUMBER);
112         if (extraValue == null) {
113             throw new IllegalStateException("Missing required version number, ignoring.");
114         }
115         return Integer.parseInt(extraValue.trim());
116     }
117 
getRequiredHashFromIntent(Intent i)118     private String getRequiredHashFromIntent(Intent i) {
119         String extraValue = i.getStringExtra(EXTRA_REQUIRED_HASH);
120         if (extraValue == null) {
121             throw new IllegalStateException("Missing required previous hash, ignoring.");
122         }
123         return extraValue.trim();
124     }
125 
getCurrentVersion()126     private int getCurrentVersion() throws NumberFormatException {
127         try {
128             String strVersion = IoUtils.readFileAsString(updateVersion.getCanonicalPath()).trim();
129             return Integer.parseInt(strVersion);
130         } catch (IOException e) {
131             Slog.i(TAG, "Couldn't find current metadata, assuming first update");
132             return 0;
133         }
134     }
135 
getAltContent(Context c, Intent i)136     private BufferedInputStream getAltContent(Context c, Intent i) throws IOException {
137         Uri content = getContentFromIntent(i);
138         Binder.allowBlockingForCurrentThread();
139         try {
140             return new BufferedInputStream(c.getContentResolver().openInputStream(content));
141         } finally {
142             Binder.defaultBlockingForCurrentThread();
143         }
144     }
145 
getCurrentContent()146     private byte[] getCurrentContent() {
147         try {
148             return IoUtils.readFileAsByteArray(updateContent.getCanonicalPath());
149         } catch (IOException e) {
150             Slog.i(TAG, "Failed to read current content, assuming first update!");
151             return null;
152         }
153     }
154 
getCurrentHash(byte[] content)155     private static String getCurrentHash(byte[] content) {
156         if (content == null) {
157             return "0";
158         }
159         try {
160             MessageDigest dgst = MessageDigest.getInstance("SHA512");
161             byte[] fingerprint = dgst.digest(content);
162             return HexDump.toHexString(fingerprint, false);
163         } catch (NoSuchAlgorithmException e) {
164             throw new AssertionError(e);
165         }
166     }
167 
verifyVersion(int current, int alternative)168     protected boolean verifyVersion(int current, int alternative) {
169         return (current < alternative);
170     }
171 
verifyPreviousHash(String current, String required)172     private boolean verifyPreviousHash(String current, String required) {
173         // this is an optional value- if the required field is NONE then we ignore it
174         if (required.equals("NONE")) {
175             return true;
176         }
177         // otherwise, verify that we match correctly
178         return current.equals(required);
179     }
180 
writeUpdate(File dir, File file, InputStream inputStream)181     protected void writeUpdate(File dir, File file, InputStream inputStream) throws IOException {
182         FileOutputStream out = null;
183         File tmp = null;
184         try {
185             // create the parents for the destination file
186             File parent = file.getParentFile();
187             parent.mkdirs();
188             // check that they were created correctly
189             if (!parent.exists()) {
190                 throw new IOException("Failed to create directory " + parent.getCanonicalPath());
191             }
192 
193             // Give executable permissions to parent folders.
194             while (!(parent.equals(updateDir))) {
195                 parent.setExecutable(true, false);
196                 parent = parent.getParentFile();
197             }
198 
199             // create the temporary file
200             tmp = File.createTempFile("journal", "", dir);
201             // mark tmp -rw-r--r--
202             tmp.setReadable(true, false);
203             // write to it
204             out = new FileOutputStream(tmp);
205             Streams.copy(inputStream, out);
206             // sync to disk
207             out.getFD().sync();
208             // atomic rename
209             if (!tmp.renameTo(file)) {
210                 throw new IOException("Failed to atomically rename " + file.getCanonicalPath());
211             }
212         } finally {
213             if (tmp != null) {
214                 tmp.delete();
215             }
216             IoUtils.closeQuietly(out);
217         }
218     }
219 
install(InputStream inputStream, int version)220     protected void install(InputStream inputStream, int version) throws IOException {
221         writeUpdate(updateDir, updateContent, inputStream);
222         writeUpdate(updateDir, updateVersion,
223                 new ByteArrayInputStream(Long.toString(version).getBytes()));
224     }
225 
postInstall(Context context, Intent intent)226     protected void postInstall(Context context, Intent intent) {
227     }
228 }
229