1 /* 2 * Copyright (C) 2015 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 org.conscrypt.ct; 18 19 import java.io.File; 20 import java.io.FileInputStream; 21 import java.io.FileNotFoundException; 22 import java.io.InputStream; 23 import java.io.StringBufferInputStream; 24 import java.nio.ByteBuffer; 25 import java.security.InvalidKeyException; 26 import java.security.NoSuchAlgorithmException; 27 import java.security.PublicKey; 28 import java.util.Arrays; 29 import java.util.Collections; 30 import java.util.HashMap; 31 import java.util.HashSet; 32 import java.util.Scanner; 33 import java.util.Set; 34 import org.conscrypt.Internal; 35 import org.conscrypt.InternalUtil; 36 37 /** 38 * @hide 39 */ 40 @Internal 41 public class CTLogStoreImpl implements CTLogStore { 42 /** 43 * Thrown when parsing of a log file fails. 44 */ 45 public static class InvalidLogFileException extends Exception { InvalidLogFileException()46 public InvalidLogFileException() { 47 } 48 InvalidLogFileException(String message)49 public InvalidLogFileException(String message) { 50 super(message); 51 } 52 InvalidLogFileException(String message, Throwable cause)53 public InvalidLogFileException(String message, Throwable cause) { 54 super(message, cause); 55 } 56 InvalidLogFileException(Throwable cause)57 public InvalidLogFileException(Throwable cause) { 58 super(cause); 59 } 60 } 61 62 private static final File defaultUserLogDir; 63 private static final File defaultSystemLogDir; 64 // Lazy loaded by CTLogStoreImpl() 65 private static volatile CTLogInfo[] defaultFallbackLogs = null; 66 static { 67 String ANDROID_DATA = System.getenv("ANDROID_DATA"); 68 String ANDROID_ROOT = System.getenv("ANDROID_ROOT"); 69 defaultUserLogDir = new File(ANDROID_DATA + "/misc/keychain/trusted_ct_logs/current/"); 70 defaultSystemLogDir = new File(ANDROID_ROOT + "/etc/security/ct_known_logs/"); 71 } 72 73 private File userLogDir; 74 private File systemLogDir; 75 private CTLogInfo[] fallbackLogs; 76 77 private HashMap<ByteBuffer, CTLogInfo> logCache = new HashMap<>(); 78 private Set<ByteBuffer> missingLogCache = Collections.synchronizedSet(new HashSet<ByteBuffer>()); 79 CTLogStoreImpl()80 public CTLogStoreImpl() { 81 this(defaultUserLogDir, 82 defaultSystemLogDir, 83 getDefaultFallbackLogs()); 84 } 85 CTLogStoreImpl(File userLogDir, File systemLogDir, CTLogInfo[] fallbackLogs)86 public CTLogStoreImpl(File userLogDir, File systemLogDir, CTLogInfo[] fallbackLogs) { 87 this.userLogDir = userLogDir; 88 this.systemLogDir = systemLogDir; 89 this.fallbackLogs = fallbackLogs; 90 } 91 92 @Override getKnownLog(byte[] logId)93 public CTLogInfo getKnownLog(byte[] logId) { 94 ByteBuffer buf = ByteBuffer.wrap(logId); 95 CTLogInfo log = logCache.get(buf); 96 if (log != null) { 97 return log; 98 } 99 if (missingLogCache.contains(buf)) { 100 return null; 101 } 102 103 log = findKnownLog(logId); 104 if (log != null) { 105 logCache.put(buf, log); 106 } else { 107 missingLogCache.add(buf); 108 } 109 110 return log; 111 } 112 findKnownLog(byte[] logId)113 private CTLogInfo findKnownLog(byte[] logId) { 114 String filename = hexEncode(logId); 115 try { 116 return loadLog(new File(userLogDir, filename)); 117 } catch (InvalidLogFileException e) { 118 return null; 119 } catch (FileNotFoundException e) {} 120 121 try { 122 return loadLog(new File(systemLogDir, filename)); 123 } catch (InvalidLogFileException e) { 124 return null; 125 } catch (FileNotFoundException e) {} 126 127 // If the updateable logs dont exist then use the fallback logs. 128 if (!userLogDir.exists()) { 129 for (CTLogInfo log: fallbackLogs) { 130 if (Arrays.equals(logId, log.getID())) { 131 return log; 132 } 133 } 134 } 135 return null; 136 } 137 getDefaultFallbackLogs()138 public static CTLogInfo[] getDefaultFallbackLogs() { 139 CTLogInfo[] result = defaultFallbackLogs; 140 if (result == null) { 141 // single-check idiom 142 defaultFallbackLogs = result = createDefaultFallbackLogs(); 143 } 144 return result; 145 } 146 createDefaultFallbackLogs()147 private static CTLogInfo[] createDefaultFallbackLogs() { 148 CTLogInfo[] logs = new CTLogInfo[KnownLogs.LOG_COUNT]; 149 for (int i = 0; i < KnownLogs.LOG_COUNT; i++) { 150 try { 151 PublicKey key = InternalUtil.logKeyToPublicKey(KnownLogs.LOG_KEYS[i]); 152 153 logs[i] = new CTLogInfo(key, 154 KnownLogs.LOG_DESCRIPTIONS[i], 155 KnownLogs.LOG_URLS[i]); 156 } catch (NoSuchAlgorithmException e) { 157 throw new RuntimeException(e); 158 } 159 } 160 161 defaultFallbackLogs = logs; 162 return logs; 163 } 164 165 /** 166 * Load a CTLogInfo from a file. 167 * @throws FileNotFoundException if the file does not exist 168 * @throws InvalidLogFileException if the file could not be parsed properly 169 * @return a CTLogInfo or null if the file is empty 170 */ loadLog(File file)171 public static CTLogInfo loadLog(File file) throws FileNotFoundException, 172 InvalidLogFileException { 173 return loadLog(new FileInputStream(file)); 174 } 175 176 /** 177 * Load a CTLogInfo from a textual representation. Closes {@code input} upon completion 178 * of loading. 179 * 180 * @throws InvalidLogFileException if the input could not be parsed properly 181 * @return a CTLogInfo or null if the input is empty 182 */ loadLog(InputStream input)183 public static CTLogInfo loadLog(InputStream input) throws InvalidLogFileException { 184 final Scanner scan = new Scanner(input, "UTF-8"); 185 scan.useDelimiter("\n"); 186 187 String description = null; 188 String url = null; 189 String key = null; 190 try { 191 // If the scanner can't even read one token then the file must be empty/blank 192 if (!scan.hasNext()) { 193 return null; 194 } 195 196 while (scan.hasNext()) { 197 String[] parts = scan.next().split(":", 2); 198 if (parts.length < 2) { 199 continue; 200 } 201 202 String name = parts[0]; 203 String value = parts[1]; 204 switch (name) { 205 case "description": 206 description = value; 207 break; 208 case "url": 209 url = value; 210 break; 211 case "key": 212 key = value; 213 break; 214 } 215 } 216 } finally { 217 scan.close(); 218 } 219 220 if (description == null || url == null || key == null) { 221 throw new InvalidLogFileException("Missing one of 'description', 'url' or 'key'"); 222 } 223 224 PublicKey pubkey; 225 try { 226 pubkey = InternalUtil.readPublicKeyPem(new StringBufferInputStream( 227 "-----BEGIN PUBLIC KEY-----\n" + 228 key + "\n" + 229 "-----END PUBLIC KEY-----")); 230 } catch (InvalidKeyException e) { 231 throw new InvalidLogFileException(e); 232 } catch (NoSuchAlgorithmException e) { 233 throw new InvalidLogFileException(e); 234 } 235 236 return new CTLogInfo(pubkey, description, url); 237 } 238 239 private final static char[] HEX_DIGITS = new char[] { 240 '0', '1', '2', '3', '4', '5', '6', '7', 241 '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' 242 }; 243 hexEncode(byte[] data)244 private static String hexEncode(byte[] data) { 245 StringBuffer sb = new StringBuffer(data.length * 2); 246 for (byte b: data) { 247 sb.append(HEX_DIGITS[(b >> 4) & 0x0f]); 248 sb.append(HEX_DIGITS[b & 0x0f]); 249 } 250 return sb.toString(); 251 } 252 } 253