17689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker/*
27689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker * Copyright (C) 2016 The Android Open Source Project
37689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker *
47689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker * Licensed under the Apache License, Version 2.0 (the "License");
57689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker * you may not use this file except in compliance with the License.
67689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker * You may obtain a copy of the License at
77689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker *
87689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker *      http://www.apache.org/licenses/LICENSE-2.0
97689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker *
107689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker * Unless required by applicable law or agreed to in writing, software
117689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker * distributed under the License is distributed on an "AS IS" BASIS,
127689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
137689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker * See the License for the specific language governing permissions and
147689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker * limitations under the License.
157689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker */
167689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker
177689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerpackage com.android.server.updates;
187689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker
197689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport com.android.internal.util.HexDump;
207689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport android.os.FileUtils;
217689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport android.system.Os;
227689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport android.system.ErrnoException;
237689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport android.util.Base64;
247689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport android.util.Slog;
257689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport java.io.File;
267689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport java.io.FileFilter;
277689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport java.io.FileOutputStream;
287689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport java.io.IOException;
297689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport java.io.OutputStreamWriter;
307689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport java.io.StringBufferInputStream;
317689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport java.nio.charset.StandardCharsets;
327689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport java.security.MessageDigest;
337689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport java.security.PublicKey;
347689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport java.security.NoSuchAlgorithmException;
357689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport org.json.JSONArray;
367689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport org.json.JSONException;
377689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerimport org.json.JSONObject;
387689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker
397689446208ae41ecf54431778647fdc83ee16fcaChad Brubakerpublic class CertificateTransparencyLogInstallReceiver extends ConfigUpdateInstallReceiver {
407689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker
417689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker    private static final String TAG = "CTLogInstallReceiver";
427689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker    private static final String LOGDIR_PREFIX = "logs-";
437689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker
447689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker    public CertificateTransparencyLogInstallReceiver() {
457689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        super("/data/misc/keychain/trusted_ct_logs/", "ct_logs", "metadata/", "version");
467689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker    }
477689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker
487689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker    @Override
497689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker    protected void install(byte[] content, int version) throws IOException {
507689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        /* Install is complicated here because we translate the input, which is a JSON file
517689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker         * containing log information to a directory with a file per log. To support atomically
527689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker         * replacing the old configuration directory with the new there's a bunch of steps. We
537689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker         * create a new directory with the logs and then do an atomic update of the current symlink
547689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker         * to point to the new directory.
557689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker         */
567689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker
577689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        // 1. Ensure that the update dir exists and is readable
587689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        updateDir.mkdir();
597689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        if (!updateDir.isDirectory()) {
607689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            throw new IOException("Unable to make directory " + updateDir.getCanonicalPath());
617689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        }
627689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        if (!updateDir.setReadable(true, false)) {
637689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            throw new IOException("Unable to set permissions on " +
647689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                    updateDir.getCanonicalPath());
657689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        }
667689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        File currentSymlink = new File(updateDir, "current");
677689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        File newVersion = new File(updateDir, LOGDIR_PREFIX + String.valueOf(version));
687689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        File oldDirectory;
697689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        // 2. Handle the corner case where the new directory already exists.
707689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        if (newVersion.exists()) {
717689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            // If the symlink has already been updated then the update died between steps 7 and 8
727689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            // and so we cannot delete the directory since its in use. Instead just bump the version
737689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            // and return.
747689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            if (newVersion.getCanonicalPath().equals(currentSymlink.getCanonicalPath())) {
757689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                writeUpdate(updateDir, updateVersion, Long.toString(version).getBytes());
767689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                deleteOldLogDirectories();
777689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                return;
787689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            } else {
797689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                FileUtils.deleteContentsAndDir(newVersion);
807689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            }
817689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        }
827689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        try {
837689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            // 3. Create /data/misc/keychain/trusted_ct_logs/<new_version>/ .
847689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            newVersion.mkdir();
857689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            if (!newVersion.isDirectory()) {
867689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                throw new IOException("Unable to make directory " + newVersion.getCanonicalPath());
877689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            }
887689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            if (!newVersion.setReadable(true, false)) {
897689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                throw new IOException("Failed to set " +newVersion.getCanonicalPath() +
907689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                        " readable");
917689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            }
927689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker
937689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            // 4. For each log in the log file create the corresponding file in <new_version>/ .
947689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            try {
957689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                JSONObject json = new JSONObject(new String(content, StandardCharsets.UTF_8));
967689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                JSONArray logs = json.getJSONArray("logs");
977689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                for (int i = 0; i < logs.length(); i++) {
987689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                    JSONObject log = logs.getJSONObject(i);
997689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                    installLog(newVersion, log);
1007689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                }
1017689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            } catch (JSONException e) {
1027689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                throw new IOException("Failed to parse logs", e);
1037689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            }
1047689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker
1057689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            // 5. Create the temp symlink. We'll rename this to the target symlink to get an atomic
1067689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            // update.
1077689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            File tempSymlink = new File(updateDir, "new_symlink");
1087689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            try {
1097689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                Os.symlink(newVersion.getCanonicalPath(), tempSymlink.getCanonicalPath());
1107689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            } catch (ErrnoException e) {
1117689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                throw new IOException("Failed to create symlink", e);
1127689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            }
1137689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker
1147689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            // 6. Update the symlink target, this is the actual update step.
1157689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            tempSymlink.renameTo(currentSymlink.getAbsoluteFile());
1167689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        } catch (IOException | RuntimeException e) {
1177689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            FileUtils.deleteContentsAndDir(newVersion);
1187689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            throw e;
1197689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        }
1207689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        Slog.i(TAG, "CT log directory updated to " + newVersion.getAbsolutePath());
1217689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        // 7. Update the current version information
1227689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        writeUpdate(updateDir, updateVersion, Long.toString(version).getBytes());
1237689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        // 8. Cleanup
1247689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        deleteOldLogDirectories();
1257689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker    }
1267689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker
1277689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker    private void installLog(File directory, JSONObject logObject) throws IOException {
1287689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        try {
1297689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            String logFilename = getLogFileName(logObject.getString("key"));
1307689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            File file = new File(directory, logFilename);
1317689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            try (OutputStreamWriter out =
1327689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                    new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) {
1337689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                writeLogEntry(out, "key", logObject.getString("key"));
1347689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                writeLogEntry(out, "url", logObject.getString("url"));
1357689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                writeLogEntry(out, "description", logObject.getString("description"));
1367689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            }
1377689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            if (!file.setReadable(true, false)) {
1387689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                throw new IOException("Failed to set permissions on " + file.getCanonicalPath());
1397689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            }
1407689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        } catch (JSONException e) {
1417689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            throw new IOException("Failed to parse log", e);
1427689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        }
1437689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker
1447689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker    }
1457689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker
1467689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker    /**
1477689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker     * Get the filename for a log based on its public key. This must be kept in sync with
1487689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker     * org.conscrypt.ct.CTLogStoreImpl.
1497689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker     */
1507689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker    private String getLogFileName(String base64PublicKey) {
1517689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        byte[] keyBytes = Base64.decode(base64PublicKey, Base64.DEFAULT);
1527689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        try {
1537689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            byte[] id = MessageDigest.getInstance("SHA-256").digest(keyBytes);
1547689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            return HexDump.toHexString(id, false);
1557689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        } catch (NoSuchAlgorithmException e) {
1567689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            // SHA-256 is guaranteed to be available.
1577689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            throw new RuntimeException(e);
1587689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        }
1597689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker    }
1607689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker
1617689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker    private void writeLogEntry(OutputStreamWriter out, String key, String value)
1627689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            throws IOException {
1637689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        out.write(key + ":" + value + "\n");
1647689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker    }
1657689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker
1667689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker    private void deleteOldLogDirectories() throws IOException {
1677689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        if (!updateDir.exists()) {
1687689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            return;
1697689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        }
1707689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        File currentTarget = new File(updateDir, "current").getCanonicalFile();
1717689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        FileFilter filter = new FileFilter() {
1727689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            @Override
1737689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            public boolean accept(File file) {
1747689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker                return !currentTarget.equals(file) && file.getName().startsWith(LOGDIR_PREFIX);
1757689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            }
1767689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        };
1777689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        for (File f : updateDir.listFiles(filter)) {
1787689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker            FileUtils.deleteContentsAndDir(f);
1797689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker        }
1807689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker    }
1817689446208ae41ecf54431778647fdc83ee16fcaChad Brubaker}
182