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