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