Home | History | Annotate | Download | only in updates
      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 
     17 package com.android.server.updates;
     18 
     19 import com.android.internal.util.HexDump;
     20 import android.os.FileUtils;
     21 import android.system.Os;
     22 import android.system.ErrnoException;
     23 import android.util.Base64;
     24 import android.util.Slog;
     25 import java.io.File;
     26 import java.io.FileFilter;
     27 import java.io.FileOutputStream;
     28 import java.io.IOException;
     29 import java.io.OutputStreamWriter;
     30 import java.io.StringBufferInputStream;
     31 import java.nio.charset.StandardCharsets;
     32 import java.security.MessageDigest;
     33 import java.security.PublicKey;
     34 import java.security.NoSuchAlgorithmException;
     35 import org.json.JSONArray;
     36 import org.json.JSONException;
     37 import org.json.JSONObject;
     38 
     39 public class CertificateTransparencyLogInstallReceiver extends ConfigUpdateInstallReceiver {
     40 
     41     private static final String TAG = "CTLogInstallReceiver";
     42     private static final String LOGDIR_PREFIX = "logs-";
     43 
     44     public CertificateTransparencyLogInstallReceiver() {
     45         super("/data/misc/keychain/trusted_ct_logs/", "ct_logs", "metadata/", "version");
     46     }
     47 
     48     @Override
     49     protected void install(byte[] content, int version) throws IOException {
     50         /* Install is complicated here because we translate the input, which is a JSON file
     51          * containing log information to a directory with a file per log. To support atomically
     52          * replacing the old configuration directory with the new there's a bunch of steps. We
     53          * create a new directory with the logs and then do an atomic update of the current symlink
     54          * to point to the new directory.
     55          */
     56 
     57         // 1. Ensure that the update dir exists and is readable
     58         updateDir.mkdir();
     59         if (!updateDir.isDirectory()) {
     60             throw new IOException("Unable to make directory " + updateDir.getCanonicalPath());
     61         }
     62         if (!updateDir.setReadable(true, false)) {
     63             throw new IOException("Unable to set permissions on " +
     64                     updateDir.getCanonicalPath());
     65         }
     66         File currentSymlink = new File(updateDir, "current");
     67         File newVersion = new File(updateDir, LOGDIR_PREFIX + String.valueOf(version));
     68         File oldDirectory;
     69         // 2. Handle the corner case where the new directory already exists.
     70         if (newVersion.exists()) {
     71             // If the symlink has already been updated then the update died between steps 7 and 8
     72             // and so we cannot delete the directory since its in use. Instead just bump the version
     73             // and return.
     74             if (newVersion.getCanonicalPath().equals(currentSymlink.getCanonicalPath())) {
     75                 writeUpdate(updateDir, updateVersion, Long.toString(version).getBytes());
     76                 deleteOldLogDirectories();
     77                 return;
     78             } else {
     79                 FileUtils.deleteContentsAndDir(newVersion);
     80             }
     81         }
     82         try {
     83             // 3. Create /data/misc/keychain/trusted_ct_logs/<new_version>/ .
     84             newVersion.mkdir();
     85             if (!newVersion.isDirectory()) {
     86                 throw new IOException("Unable to make directory " + newVersion.getCanonicalPath());
     87             }
     88             if (!newVersion.setReadable(true, false)) {
     89                 throw new IOException("Failed to set " +newVersion.getCanonicalPath() +
     90                         " readable");
     91             }
     92 
     93             // 4. For each log in the log file create the corresponding file in <new_version>/ .
     94             try {
     95                 JSONObject json = new JSONObject(new String(content, StandardCharsets.UTF_8));
     96                 JSONArray logs = json.getJSONArray("logs");
     97                 for (int i = 0; i < logs.length(); i++) {
     98                     JSONObject log = logs.getJSONObject(i);
     99                     installLog(newVersion, log);
    100                 }
    101             } catch (JSONException e) {
    102                 throw new IOException("Failed to parse logs", e);
    103             }
    104 
    105             // 5. Create the temp symlink. We'll rename this to the target symlink to get an atomic
    106             // update.
    107             File tempSymlink = new File(updateDir, "new_symlink");
    108             try {
    109                 Os.symlink(newVersion.getCanonicalPath(), tempSymlink.getCanonicalPath());
    110             } catch (ErrnoException e) {
    111                 throw new IOException("Failed to create symlink", e);
    112             }
    113 
    114             // 6. Update the symlink target, this is the actual update step.
    115             tempSymlink.renameTo(currentSymlink.getAbsoluteFile());
    116         } catch (IOException | RuntimeException e) {
    117             FileUtils.deleteContentsAndDir(newVersion);
    118             throw e;
    119         }
    120         Slog.i(TAG, "CT log directory updated to " + newVersion.getAbsolutePath());
    121         // 7. Update the current version information
    122         writeUpdate(updateDir, updateVersion, Long.toString(version).getBytes());
    123         // 8. Cleanup
    124         deleteOldLogDirectories();
    125     }
    126 
    127     private void installLog(File directory, JSONObject logObject) throws IOException {
    128         try {
    129             String logFilename = getLogFileName(logObject.getString("key"));
    130             File file = new File(directory, logFilename);
    131             try (OutputStreamWriter out =
    132                     new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) {
    133                 writeLogEntry(out, "key", logObject.getString("key"));
    134                 writeLogEntry(out, "url", logObject.getString("url"));
    135                 writeLogEntry(out, "description", logObject.getString("description"));
    136             }
    137             if (!file.setReadable(true, false)) {
    138                 throw new IOException("Failed to set permissions on " + file.getCanonicalPath());
    139             }
    140         } catch (JSONException e) {
    141             throw new IOException("Failed to parse log", e);
    142         }
    143 
    144     }
    145 
    146     /**
    147      * Get the filename for a log based on its public key. This must be kept in sync with
    148      * org.conscrypt.ct.CTLogStoreImpl.
    149      */
    150     private String getLogFileName(String base64PublicKey) {
    151         byte[] keyBytes = Base64.decode(base64PublicKey, Base64.DEFAULT);
    152         try {
    153             byte[] id = MessageDigest.getInstance("SHA-256").digest(keyBytes);
    154             return HexDump.toHexString(id, false);
    155         } catch (NoSuchAlgorithmException e) {
    156             // SHA-256 is guaranteed to be available.
    157             throw new RuntimeException(e);
    158         }
    159     }
    160 
    161     private void writeLogEntry(OutputStreamWriter out, String key, String value)
    162             throws IOException {
    163         out.write(key + ":" + value + "\n");
    164     }
    165 
    166     private void deleteOldLogDirectories() throws IOException {
    167         if (!updateDir.exists()) {
    168             return;
    169         }
    170         File currentTarget = new File(updateDir, "current").getCanonicalFile();
    171         FileFilter filter = new FileFilter() {
    172             @Override
    173             public boolean accept(File file) {
    174                 return !currentTarget.equals(file) && file.getName().startsWith(LOGDIR_PREFIX);
    175             }
    176         };
    177         for (File f : updateDir.listFiles(filter)) {
    178             FileUtils.deleteContentsAndDir(f);
    179         }
    180     }
    181 }
    182