blob: ec65f8dafe52385469aad41c35336af4bd7a70bc [file] [log] [blame]
Chad Brubaker76894462016-08-10 10:40:45 -07001/*
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
17package com.android.server.updates;
18
19import com.android.internal.util.HexDump;
20import android.os.FileUtils;
21import android.system.Os;
22import android.system.ErrnoException;
23import android.util.Base64;
24import android.util.Slog;
25import java.io.File;
26import java.io.FileFilter;
27import java.io.FileOutputStream;
28import java.io.IOException;
29import java.io.OutputStreamWriter;
30import java.io.StringBufferInputStream;
31import java.nio.charset.StandardCharsets;
32import java.security.MessageDigest;
33import java.security.PublicKey;
34import java.security.NoSuchAlgorithmException;
35import org.json.JSONArray;
36import org.json.JSONException;
37import org.json.JSONObject;
38
39public 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}