1 /*
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 
17 package com.android.tradefed.util.keystore;
18 
19 import com.android.tradefed.config.Option;
20 import com.android.tradefed.config.Option.Importance;
21 import com.android.tradefed.config.OptionClass;
22 import com.android.tradefed.log.LogUtil.CLog;
23 import com.android.tradefed.util.FileUtil;
24 
25 import com.google.common.annotations.VisibleForTesting;
26 
27 import org.json.JSONException;
28 import org.json.JSONObject;
29 
30 import java.io.File;
31 import java.io.IOException;
32 import java.net.InetAddress;
33 import java.net.UnknownHostException;
34 import java.util.ArrayList;
35 import java.util.Iterator;
36 import java.util.List;
37 import java.util.regex.Pattern;
38 
39 /**
40  * Implementation of a JSON KeyStore Factory, which provides a {@link JSONFileKeyStoreClient} for
41  * accessing a JSON Key Store File.
42  */
43 @OptionClass(alias = "json-keystore")
44 public class JSONFileKeyStoreFactory implements IKeyStoreFactory {
45     @Option(
46             name = "json-key-store-file",
47             description = "The JSON file from where to read the key store",
48             importance = Importance.IF_UNSET)
49     private File mJsonFile = null;
50 
51     @Option(
52             name = "host-based-key-store-file",
53             description = "The JSON file from where to read the host-based key store",
54             importance = Importance.IF_UNSET)
55     private List<File> mHostBasedJsonFiles = new ArrayList<File>();
56 
57     /** Keystore factory is a global object and may be accessed concurrently so we need a lock */
58     private static final Object mLock = new Object();
59 
60     private JSONFileKeyStoreClient mCachedClient = null;
61     private long mLastLoadedTime = 0l;
62     private String mHostName = null;
63 
64     /** {@inheritDoc} */
65     @Override
createKeyStoreClient()66     public IKeyStoreClient createKeyStoreClient() throws KeyStoreException {
67         synchronized (mLock) {
68             mHostName = getHostName();
69 
70             List<String> invalidFiles = findInvalidJsonKeyStoreFiles();
71             if (mCachedClient == null) {
72                 if (!invalidFiles.isEmpty()) {
73                     throw new KeyStoreException(
74                             String.format(
75                                     "These keystore files are missing: %s",
76                                     String.join("\n", invalidFiles)));
77                 }
78 
79                 createKeyStoreInternal();
80                 CLog.d(
81                         "Keystore initialized with %s and %d host-based keystore files.",
82                         mJsonFile.getAbsolutePath(), mHostBasedJsonFiles.size());
83             }
84             if (!invalidFiles.isEmpty()) {
85                 CLog.w(
86                         String.format(
87                                 "These keystore files are missing: %s",
88                                 String.join("\n", invalidFiles)));
89             } else {
90                 List<String> changedFiles = findChangedJsonKeyStoreFiles();
91                 if (!changedFiles.isEmpty()) {
92                     createKeyStoreInternal();
93                     CLog.d(
94                             "Reloading the keystore as these keystore files have changed: %s",
95                             String.join("\n", changedFiles));
96                 }
97             }
98 
99             return mCachedClient;
100         }
101     }
102 
getHostName()103     private String getHostName() throws KeyStoreException {
104         if (mHostName == null) {
105             try {
106                 mHostName = InetAddress.getLocalHost().getHostName();
107             } catch (UnknownHostException e) {
108                 throw new KeyStoreException(String.format("Failed to get hostname"), e);
109             }
110         }
111 
112         return mHostName;
113     }
114 
findInvalidJsonKeyStoreFiles()115     private List<String> findInvalidJsonKeyStoreFiles() {
116         List<String> invalidFiles = new ArrayList<String>(); // not exist or not canRead().
117         if (!mJsonFile.exists() || !mJsonFile.canRead()) {
118             invalidFiles.add(mJsonFile.getAbsolutePath());
119         }
120         for (File file : mHostBasedJsonFiles) {
121             if (!file.exists() || !file.canRead()) {
122                 invalidFiles.add(file.getAbsolutePath());
123             }
124         }
125         return invalidFiles;
126     }
127 
findChangedJsonKeyStoreFiles()128     private List<String> findChangedJsonKeyStoreFiles() {
129         List<String> changedFiles = new ArrayList<String>();
130         if (mLastLoadedTime < mJsonFile.lastModified()) {
131             changedFiles.add(mJsonFile.getAbsolutePath());
132         }
133         for (File file : mHostBasedJsonFiles) {
134             if (mLastLoadedTime < file.lastModified()) {
135                 changedFiles.add(file.getAbsolutePath());
136             }
137         }
138         return changedFiles;
139     }
140 
createKeyStoreInternal()141     private void createKeyStoreInternal() throws KeyStoreException {
142         mLastLoadedTime = System.currentTimeMillis();
143         mCachedClient = new JSONFileKeyStoreClient(mJsonFile);
144         for (File file : mHostBasedJsonFiles) {
145             overrideClientWithHostKeyStoreFromFile(file);
146         }
147     }
148 
overrideClientWithHostKeyStoreFromFile(File file)149     private void overrideClientWithHostKeyStoreFromFile(File file) throws KeyStoreException {
150         JSONObject hostKeyStore = getHostKeyStoreFromFile(file);
151         if (hostKeyStore == null) {
152             // This is not necessarily an error.
153             CLog.d(
154                     "Host-based keystore for %s not found in Keystore file %s",
155                     mHostName, mJsonFile.getAbsolutePath());
156             return;
157         }
158         for (Iterator<String> keys = hostKeyStore.keys(); keys.hasNext(); ) {
159             String key = keys.next();
160             try {
161                 mCachedClient.setKey(key, hostKeyStore.getString(key));
162                 CLog.d(
163                         "Hostname: %s, %s gets value from file:%s",
164                         mHostName, key, mJsonFile.getAbsolutePath());
165             } catch (JSONException e) {
166                 throw new KeyStoreException(
167                         String.format(
168                                 "Failed to update keystore with host-based keystore from file"
169                                         + " %s",
170                                 file.toString()),
171                         e);
172             }
173         }
174     }
175 
getHostKeyStoreFromFile(File file)176     private JSONObject getHostKeyStoreFromFile(File file) throws KeyStoreException {
177         JSONObject hostKeyStore = null;
178         JSONObject jsonKeyStore = loadKeyStoreFromFile(file);
179         int matchingPatternCount = 0;
180         for (Iterator<String> patternStrings = jsonKeyStore.keys(); patternStrings.hasNext(); ) {
181             String patternString = patternStrings.next();
182             Pattern pattern = Pattern.compile(patternString);
183             if (pattern.matcher(mHostName).matches()) {
184                 CLog.d(
185                         "Hostname %s matches pattern string %s in file %s.",
186                         mHostName, patternString, file.getAbsolutePath());
187 
188                 if (++matchingPatternCount > 1) {
189                     throw new KeyStoreException(
190                             String.format(
191                                     "Hostname %s matches multiple pattern strings in file %s.",
192                                     mHostName, file.toString()));
193                 }
194 
195                 try {
196                     hostKeyStore = jsonKeyStore.getJSONObject(patternString);
197                 } catch (JSONException e) {
198                     throw new KeyStoreException(
199                             String.format(
200                                     "Failed to parse JSON data from file %s", file.toString()),
201                             e);
202                 }
203             }
204         }
205         return hostKeyStore;
206     }
207 
loadKeyStoreFromFile(File jsonFile)208     private JSONObject loadKeyStoreFromFile(File jsonFile) throws KeyStoreException {
209         JSONObject keyStore = null;
210         try {
211             String data = FileUtil.readStringFromFile(jsonFile);
212             keyStore = new JSONObject(data);
213         } catch (IOException e) {
214             throw new KeyStoreException(
215                     String.format("Failed to read JSON key file %s: %s", jsonFile.toString(), e));
216         } catch (JSONException e) {
217             throw new KeyStoreException(
218                     String.format("Failed to parse JSON data from file %s", jsonFile.toString()),
219                     e);
220         }
221         return keyStore;
222     }
223 
224     /**
225      * Helper method used to set host name. Used for testing.
226      *
227      * @param hostName to use as host name.
228      */
229     @VisibleForTesting
setHostName(String hostName)230     public void setHostName(String hostName) {
231         mHostName = hostName;
232     }
233 }
234