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