• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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