| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.updates; |
| |
| import com.android.internal.util.HexDump; |
| import android.os.FileUtils; |
| import android.system.Os; |
| import android.system.ErrnoException; |
| import android.util.Base64; |
| import android.util.Slog; |
| import java.io.File; |
| import java.io.FileFilter; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStreamWriter; |
| import java.io.StringBufferInputStream; |
| import java.nio.charset.StandardCharsets; |
| import java.security.MessageDigest; |
| import java.security.PublicKey; |
| import java.security.NoSuchAlgorithmException; |
| import org.json.JSONArray; |
| import org.json.JSONException; |
| import org.json.JSONObject; |
| |
| public class CertificateTransparencyLogInstallReceiver extends ConfigUpdateInstallReceiver { |
| |
| private static final String TAG = "CTLogInstallReceiver"; |
| private static final String LOGDIR_PREFIX = "logs-"; |
| |
| public CertificateTransparencyLogInstallReceiver() { |
| super("/data/misc/keychain/trusted_ct_logs/", "ct_logs", "metadata/", "version"); |
| } |
| |
| @Override |
| protected void install(byte[] content, int version) throws IOException { |
| /* Install is complicated here because we translate the input, which is a JSON file |
| * containing log information to a directory with a file per log. To support atomically |
| * replacing the old configuration directory with the new there's a bunch of steps. We |
| * create a new directory with the logs and then do an atomic update of the current symlink |
| * to point to the new directory. |
| */ |
| |
| // 1. Ensure that the update dir exists and is readable |
| updateDir.mkdir(); |
| if (!updateDir.isDirectory()) { |
| throw new IOException("Unable to make directory " + updateDir.getCanonicalPath()); |
| } |
| if (!updateDir.setReadable(true, false)) { |
| throw new IOException("Unable to set permissions on " + |
| updateDir.getCanonicalPath()); |
| } |
| File currentSymlink = new File(updateDir, "current"); |
| File newVersion = new File(updateDir, LOGDIR_PREFIX + String.valueOf(version)); |
| File oldDirectory; |
| // 2. Handle the corner case where the new directory already exists. |
| if (newVersion.exists()) { |
| // If the symlink has already been updated then the update died between steps 7 and 8 |
| // and so we cannot delete the directory since its in use. Instead just bump the version |
| // and return. |
| if (newVersion.getCanonicalPath().equals(currentSymlink.getCanonicalPath())) { |
| writeUpdate(updateDir, updateVersion, Long.toString(version).getBytes()); |
| deleteOldLogDirectories(); |
| return; |
| } else { |
| FileUtils.deleteContentsAndDir(newVersion); |
| } |
| } |
| try { |
| // 3. Create /data/misc/keychain/trusted_ct_logs/<new_version>/ . |
| newVersion.mkdir(); |
| if (!newVersion.isDirectory()) { |
| throw new IOException("Unable to make directory " + newVersion.getCanonicalPath()); |
| } |
| if (!newVersion.setReadable(true, false)) { |
| throw new IOException("Failed to set " +newVersion.getCanonicalPath() + |
| " readable"); |
| } |
| |
| // 4. For each log in the log file create the corresponding file in <new_version>/ . |
| try { |
| JSONObject json = new JSONObject(new String(content, StandardCharsets.UTF_8)); |
| JSONArray logs = json.getJSONArray("logs"); |
| for (int i = 0; i < logs.length(); i++) { |
| JSONObject log = logs.getJSONObject(i); |
| installLog(newVersion, log); |
| } |
| } catch (JSONException e) { |
| throw new IOException("Failed to parse logs", e); |
| } |
| |
| // 5. Create the temp symlink. We'll rename this to the target symlink to get an atomic |
| // update. |
| File tempSymlink = new File(updateDir, "new_symlink"); |
| try { |
| Os.symlink(newVersion.getCanonicalPath(), tempSymlink.getCanonicalPath()); |
| } catch (ErrnoException e) { |
| throw new IOException("Failed to create symlink", e); |
| } |
| |
| // 6. Update the symlink target, this is the actual update step. |
| tempSymlink.renameTo(currentSymlink.getAbsoluteFile()); |
| } catch (IOException | RuntimeException e) { |
| FileUtils.deleteContentsAndDir(newVersion); |
| throw e; |
| } |
| Slog.i(TAG, "CT log directory updated to " + newVersion.getAbsolutePath()); |
| // 7. Update the current version information |
| writeUpdate(updateDir, updateVersion, Long.toString(version).getBytes()); |
| // 8. Cleanup |
| deleteOldLogDirectories(); |
| } |
| |
| private void installLog(File directory, JSONObject logObject) throws IOException { |
| try { |
| String logFilename = getLogFileName(logObject.getString("key")); |
| File file = new File(directory, logFilename); |
| try (OutputStreamWriter out = |
| new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) { |
| writeLogEntry(out, "key", logObject.getString("key")); |
| writeLogEntry(out, "url", logObject.getString("url")); |
| writeLogEntry(out, "description", logObject.getString("description")); |
| } |
| if (!file.setReadable(true, false)) { |
| throw new IOException("Failed to set permissions on " + file.getCanonicalPath()); |
| } |
| } catch (JSONException e) { |
| throw new IOException("Failed to parse log", e); |
| } |
| |
| } |
| |
| /** |
| * Get the filename for a log based on its public key. This must be kept in sync with |
| * org.conscrypt.ct.CTLogStoreImpl. |
| */ |
| private String getLogFileName(String base64PublicKey) { |
| byte[] keyBytes = Base64.decode(base64PublicKey, Base64.DEFAULT); |
| try { |
| byte[] id = MessageDigest.getInstance("SHA-256").digest(keyBytes); |
| return HexDump.toHexString(id, false); |
| } catch (NoSuchAlgorithmException e) { |
| // SHA-256 is guaranteed to be available. |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private void writeLogEntry(OutputStreamWriter out, String key, String value) |
| throws IOException { |
| out.write(key + ":" + value + "\n"); |
| } |
| |
| private void deleteOldLogDirectories() throws IOException { |
| if (!updateDir.exists()) { |
| return; |
| } |
| File currentTarget = new File(updateDir, "current").getCanonicalFile(); |
| FileFilter filter = new FileFilter() { |
| @Override |
| public boolean accept(File file) { |
| return !currentTarget.equals(file) && file.getName().startsWith(LOGDIR_PREFIX); |
| } |
| }; |
| for (File f : updateDir.listFiles(filter)) { |
| FileUtils.deleteContentsAndDir(f); |
| } |
| } |
| } |