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.server.wifi; 18 19 import android.app.AlarmManager; 20 import android.content.Context; 21 import android.os.Environment; 22 import android.os.FileUtils; 23 import android.os.Handler; 24 import android.os.Looper; 25 import android.util.Log; 26 import android.util.Xml; 27 28 import com.android.internal.annotations.VisibleForTesting; 29 import com.android.internal.os.AtomicFile; 30 import com.android.internal.util.FastXmlSerializer; 31 import com.android.server.wifi.util.XmlUtil; 32 33 import org.xmlpull.v1.XmlPullParser; 34 import org.xmlpull.v1.XmlPullParserException; 35 import org.xmlpull.v1.XmlSerializer; 36 37 import java.io.ByteArrayInputStream; 38 import java.io.ByteArrayOutputStream; 39 import java.io.File; 40 import java.io.FileNotFoundException; 41 import java.io.FileOutputStream; 42 import java.io.IOException; 43 import java.nio.charset.StandardCharsets; 44 import java.util.HashMap; 45 import java.util.Map; 46 47 /** 48 * This class provides the API's to save/load/modify network configurations from a persistent 49 * store. Uses keystore for certificate/key management operations. 50 * NOTE: This class should only be used from WifiConfigManager and is not thread-safe! 51 */ 52 public class WifiConfigStore { 53 private static final String XML_TAG_DOCUMENT_HEADER = "WifiConfigStoreData"; 54 private static final String XML_TAG_VERSION = "Version"; 55 /** 56 * Current config store data version. This will be incremented for any additions. 57 */ 58 private static final int CURRENT_CONFIG_STORE_DATA_VERSION = 1; 59 /** This list of older versions will be used to restore data from older config store. */ 60 /** 61 * First version of the config store data format. 62 */ 63 private static final int INITIAL_CONFIG_STORE_DATA_VERSION = 1; 64 65 /** 66 * Alarm tag to use for starting alarms for buffering file writes. 67 */ 68 @VisibleForTesting 69 public static final String BUFFERED_WRITE_ALARM_TAG = "WriteBufferAlarm"; 70 /** 71 * Log tag. 72 */ 73 private static final String TAG = "WifiConfigStore"; 74 /** 75 * Config store file name for both shared & user specific stores. 76 */ 77 private static final String STORE_FILE_NAME = "WifiConfigStore.xml"; 78 /** 79 * Directory to store the config store files in. 80 */ 81 private static final String STORE_DIRECTORY_NAME = "wifi"; 82 /** 83 * Time interval for buffering file writes for non-forced writes 84 */ 85 private static final int BUFFERED_WRITE_ALARM_INTERVAL_MS = 10 * 1000; 86 /** 87 * Handler instance to post alarm timeouts to 88 */ 89 private final Handler mEventHandler; 90 /** 91 * Alarm manager instance to start buffer timeout alarms. 92 */ 93 private final AlarmManager mAlarmManager; 94 /** 95 * Clock instance to retrieve timestamps for alarms. 96 */ 97 private final Clock mClock; 98 /** 99 * Shared config store file instance. 100 */ 101 private StoreFile mSharedStore; 102 /** 103 * User specific store file instance. 104 */ 105 private StoreFile mUserStore; 106 /** 107 * Verbose logging flag. 108 */ 109 private boolean mVerboseLoggingEnabled = false; 110 /** 111 * Flag to indicate if there is a buffered write pending. 112 */ 113 private boolean mBufferedWritePending = false; 114 /** 115 * Alarm listener for flushing out any buffered writes. 116 */ 117 private final AlarmManager.OnAlarmListener mBufferedWriteListener = 118 new AlarmManager.OnAlarmListener() { 119 public void onAlarm() { 120 try { 121 writeBufferedData(); 122 } catch (IOException e) { 123 Log.wtf(TAG, "Buffered write failed", e); 124 } 125 126 } 127 }; 128 129 /** 130 * List of data container. 131 */ 132 private final Map<String, StoreData> mStoreDataList; 133 134 /** 135 * Create a new instance of WifiConfigStore. 136 * Note: The store file instances have been made inputs to this class to ease unit-testing. 137 * 138 * @param context context to use for retrieving the alarm manager. 139 * @param looper looper instance to post alarm timeouts to. 140 * @param clock clock instance to retrieve timestamps for alarms. 141 * @param sharedStore StoreFile instance pointing to the shared store file. This should 142 * be retrieved using {@link #createSharedFile()} method. 143 */ WifiConfigStore(Context context, Looper looper, Clock clock, StoreFile sharedStore)144 public WifiConfigStore(Context context, Looper looper, Clock clock, 145 StoreFile sharedStore) { 146 147 mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 148 mEventHandler = new Handler(looper); 149 mClock = clock; 150 mStoreDataList = new HashMap<>(); 151 152 // Initialize the store files. 153 mSharedStore = sharedStore; 154 // The user store is initialized to null, this will be set when the user unlocks and 155 // CE storage is accessible via |switchUserStoreAndRead|. 156 mUserStore = null; 157 } 158 setUserStore(StoreFile userStore)159 public void setUserStore(StoreFile userStore) { 160 mUserStore = userStore; 161 } 162 163 /** 164 * Register a {@link StoreData} to store. A {@link StoreData} is responsible 165 * for a block of data in the store file, and provides serialization/deserialization functions 166 * for those data. 167 * 168 * @param storeData The store data to be registered to the config store 169 * @return true if succeeded 170 */ registerStoreData(StoreData storeData)171 public boolean registerStoreData(StoreData storeData) { 172 if (storeData == null) { 173 Log.e(TAG, "Unable to register null store data"); 174 return false; 175 } 176 mStoreDataList.put(storeData.getName(), storeData); 177 return true; 178 } 179 180 /** 181 * Helper method to create a store file instance for either the shared store or user store. 182 * Note: The method creates the store directory if not already present. This may be needed for 183 * user store files. 184 * 185 * @param storeBaseDir Base directory under which the store file is to be stored. The store file 186 * will be at <storeBaseDir>/wifi/WifiConfigStore.xml. 187 * @return new instance of the store file. 188 */ createFile(File storeBaseDir)189 private static StoreFile createFile(File storeBaseDir) { 190 File storeDir = new File(storeBaseDir, STORE_DIRECTORY_NAME); 191 if (!storeDir.exists()) { 192 if (!storeDir.mkdir()) { 193 Log.w(TAG, "Could not create store directory " + storeDir); 194 } 195 } 196 return new StoreFile(new File(storeDir, STORE_FILE_NAME)); 197 } 198 199 /** 200 * Create a new instance of the shared store file. 201 * 202 * @return new instance of the store file or null if the directory cannot be created. 203 */ createSharedFile()204 public static StoreFile createSharedFile() { 205 return createFile(Environment.getDataMiscDirectory()); 206 } 207 208 /** 209 * Create a new instance of the user specific store file. 210 * The user store file is inside the user's encrypted data directory. 211 * 212 * @param userId userId corresponding to the currently logged-in user. 213 * @return new instance of the store file or null if the directory cannot be created. 214 */ createUserFile(int userId)215 public static StoreFile createUserFile(int userId) { 216 return createFile(Environment.getDataMiscCeDirectory(userId)); 217 } 218 219 /** 220 * Enable verbose logging. 221 */ enableVerboseLogging(boolean verbose)222 public void enableVerboseLogging(boolean verbose) { 223 mVerboseLoggingEnabled = verbose; 224 } 225 226 /** 227 * API to check if any of the store files are present on the device. This can be used 228 * to detect if the device needs to perform data migration from legacy stores. 229 * 230 * @return true if any of the store file is present, false otherwise. 231 */ areStoresPresent()232 public boolean areStoresPresent() { 233 return (mSharedStore.exists() || (mUserStore != null && mUserStore.exists())); 234 } 235 236 /** 237 * API to write the data provided by registered store data to config stores. 238 * The method writes the user specific configurations to user specific config store and the 239 * shared configurations to shared config store. 240 * 241 * @param forceSync boolean to force write the config stores now. if false, the writes are 242 * buffered and written after the configured interval. 243 */ write(boolean forceSync)244 public void write(boolean forceSync) 245 throws XmlPullParserException, IOException { 246 // Serialize the provided data and send it to the respective stores. The actual write will 247 // be performed later depending on the |forceSync| flag . 248 byte[] sharedDataBytes = serializeData(true); 249 mSharedStore.storeRawDataToWrite(sharedDataBytes); 250 if (mUserStore != null) { 251 byte[] userDataBytes = serializeData(false); 252 mUserStore.storeRawDataToWrite(userDataBytes); 253 } 254 255 // Every write provides a new snapshot to be persisted, so |forceSync| flag overrides any 256 // pending buffer writes. 257 if (forceSync) { 258 writeBufferedData(); 259 } else { 260 startBufferedWriteAlarm(); 261 } 262 } 263 264 /** 265 * Serialize share data or user data from all store data. 266 * 267 * @param shareData Flag indicating share data 268 * @return byte[] of serialized bytes 269 * @throws XmlPullParserException 270 * @throws IOException 271 */ serializeData(boolean shareData)272 private byte[] serializeData(boolean shareData) throws XmlPullParserException, IOException { 273 final XmlSerializer out = new FastXmlSerializer(); 274 final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 275 out.setOutput(outputStream, StandardCharsets.UTF_8.name()); 276 277 XmlUtil.writeDocumentStart(out, XML_TAG_DOCUMENT_HEADER); 278 XmlUtil.writeNextValue(out, XML_TAG_VERSION, CURRENT_CONFIG_STORE_DATA_VERSION); 279 280 for (Map.Entry<String, StoreData> entry : mStoreDataList.entrySet()) { 281 String tag = entry.getKey(); 282 StoreData storeData = entry.getValue(); 283 // Ignore this store data if this is for share file and the store data doesn't support 284 // share store. 285 if (shareData && !storeData.supportShareData()) { 286 continue; 287 } 288 XmlUtil.writeNextSectionStart(out, tag); 289 storeData.serializeData(out, shareData); 290 XmlUtil.writeNextSectionEnd(out, tag); 291 } 292 XmlUtil.writeDocumentEnd(out, XML_TAG_DOCUMENT_HEADER); 293 294 return outputStream.toByteArray(); 295 } 296 297 /** 298 * Helper method to start a buffered write alarm if one doesn't already exist. 299 */ startBufferedWriteAlarm()300 private void startBufferedWriteAlarm() { 301 if (!mBufferedWritePending) { 302 mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 303 mClock.getElapsedSinceBootMillis() + BUFFERED_WRITE_ALARM_INTERVAL_MS, 304 BUFFERED_WRITE_ALARM_TAG, mBufferedWriteListener, mEventHandler); 305 mBufferedWritePending = true; 306 } 307 } 308 309 /** 310 * Helper method to stop a buffered write alarm if one exists. 311 */ stopBufferedWriteAlarm()312 private void stopBufferedWriteAlarm() { 313 if (mBufferedWritePending) { 314 mAlarmManager.cancel(mBufferedWriteListener); 315 mBufferedWritePending = false; 316 } 317 } 318 319 /** 320 * Helper method to actually perform the writes to the file. This flushes out any write data 321 * being buffered in the respective stores and cancels any pending buffer write alarms. 322 */ writeBufferedData()323 private void writeBufferedData() throws IOException { 324 stopBufferedWriteAlarm(); 325 326 long writeStartTime = mClock.getElapsedSinceBootMillis(); 327 mSharedStore.writeBufferedRawData(); 328 if (mUserStore != null) { 329 mUserStore.writeBufferedRawData(); 330 } 331 long writeTime = mClock.getElapsedSinceBootMillis() - writeStartTime; 332 333 Log.d(TAG, "Writing to stores completed in " + writeTime + " ms."); 334 } 335 336 /** 337 * API to read the store data from the config stores. 338 * The method reads the user specific configurations from user specific config store and the 339 * shared configurations from the shared config store. 340 */ read()341 public void read() throws XmlPullParserException, IOException { 342 // Reset both share and user store data. 343 resetStoreData(true); 344 resetStoreData(false); 345 346 long readStartTime = mClock.getElapsedSinceBootMillis(); 347 byte[] sharedDataBytes = mSharedStore.readRawData(); 348 byte[] userDataBytes = null; 349 if (mUserStore != null) { 350 userDataBytes = mUserStore.readRawData(); 351 } 352 long readTime = mClock.getElapsedSinceBootMillis() - readStartTime; 353 Log.d(TAG, "Reading from stores completed in " + readTime + " ms."); 354 deserializeData(sharedDataBytes, true); 355 deserializeData(userDataBytes, false); 356 } 357 358 /** 359 * Handles a user switch. This method changes the user specific store file and reads from the 360 * new user's store file. 361 * 362 * @param userStore StoreFile instance pointing to the user specific store file. This should 363 * be retrieved using {@link #createUserFile(int)} method. 364 */ switchUserStoreAndRead(StoreFile userStore)365 public void switchUserStoreAndRead(StoreFile userStore) 366 throws XmlPullParserException, IOException { 367 // Reset user store data. 368 resetStoreData(false); 369 370 // Stop any pending buffered writes, if any. 371 stopBufferedWriteAlarm(); 372 mUserStore = userStore; 373 374 // Now read from the user store file. 375 long readStartTime = mClock.getElapsedSinceBootMillis(); 376 byte[] userDataBytes = mUserStore.readRawData(); 377 long readTime = mClock.getElapsedSinceBootMillis() - readStartTime; 378 Log.d(TAG, "Reading from user store completed in " + readTime + " ms."); 379 deserializeData(userDataBytes, false); 380 } 381 382 /** 383 * Reset share data or user data in all store data. 384 * 385 * @param shareData Flag indicating share data 386 */ resetStoreData(boolean shareData)387 private void resetStoreData(boolean shareData) { 388 for (Map.Entry<String, StoreData> entry : mStoreDataList.entrySet()) { 389 entry.getValue().resetData(shareData); 390 } 391 } 392 393 /** 394 * Deserialize share data or user data into store data. 395 * 396 * @param dataBytes The data to parse 397 * @param shareData The flag indicating share data 398 * @throws XmlPullParserException 399 * @throws IOException 400 */ deserializeData(byte[] dataBytes, boolean shareData)401 private void deserializeData(byte[] dataBytes, boolean shareData) 402 throws XmlPullParserException, IOException { 403 if (dataBytes == null) { 404 return; 405 } 406 final XmlPullParser in = Xml.newPullParser(); 407 final ByteArrayInputStream inputStream = new ByteArrayInputStream(dataBytes); 408 in.setInput(inputStream, StandardCharsets.UTF_8.name()); 409 410 // Start parsing the XML stream. 411 int rootTagDepth = in.getDepth() + 1; 412 parseDocumentStartAndVersionFromXml(in); 413 414 String[] headerName = new String[1]; 415 while (XmlUtil.gotoNextSectionOrEnd(in, headerName, rootTagDepth)) { 416 StoreData storeData = mStoreDataList.get(headerName[0]); 417 if (storeData == null) { 418 throw new XmlPullParserException("Unknown store data: " + headerName[0]); 419 } 420 storeData.deserializeData(in, rootTagDepth + 1, shareData); 421 } 422 } 423 424 /** 425 * Parse the document start and version from the XML stream. 426 * This is used for both the shared and user config store data. 427 * 428 * @param in XmlPullParser instance pointing to the XML stream. 429 * @return version number retrieved from the Xml stream. 430 */ parseDocumentStartAndVersionFromXml(XmlPullParser in)431 private static int parseDocumentStartAndVersionFromXml(XmlPullParser in) 432 throws XmlPullParserException, IOException { 433 XmlUtil.gotoDocumentStart(in, XML_TAG_DOCUMENT_HEADER); 434 int version = (int) XmlUtil.readNextValueWithName(in, XML_TAG_VERSION); 435 if (version < INITIAL_CONFIG_STORE_DATA_VERSION 436 || version > CURRENT_CONFIG_STORE_DATA_VERSION) { 437 throw new XmlPullParserException("Invalid version of data: " + version); 438 } 439 return version; 440 } 441 442 /** 443 * Class to encapsulate all file writes. This is a wrapper over {@link AtomicFile} to write/read 444 * raw data from the persistent file. This class provides helper methods to read/write the 445 * entire file into a byte array. 446 * This helps to separate out the processing/parsing from the actual file writing. 447 */ 448 public static class StoreFile { 449 /** 450 * File permissions to lock down the file. 451 */ 452 private static final int FILE_MODE = 0600; 453 /** 454 * The store file to be written to. 455 */ 456 private final AtomicFile mAtomicFile; 457 /** 458 * This is an intermediate buffer to store the data to be written. 459 */ 460 private byte[] mWriteData; 461 /** 462 * Store the file name for setting the file permissions/logging purposes. 463 */ 464 private String mFileName; 465 StoreFile(File file)466 public StoreFile(File file) { 467 mAtomicFile = new AtomicFile(file); 468 mFileName = mAtomicFile.getBaseFile().getAbsolutePath(); 469 } 470 471 /** 472 * Returns whether the store file already exists on disk or not. 473 * 474 * @return true if it exists, false otherwise. 475 */ exists()476 public boolean exists() { 477 return mAtomicFile.exists(); 478 } 479 480 /** 481 * Read the entire raw data from the store file and return in a byte array. 482 * 483 * @return raw data read from the file or null if the file is not found. 484 * @throws IOException if an error occurs. The input stream is always closed by the method 485 * even when an exception is encountered. 486 */ readRawData()487 public byte[] readRawData() throws IOException { 488 try { 489 return mAtomicFile.readFully(); 490 } catch (FileNotFoundException e) { 491 return null; 492 } 493 } 494 495 /** 496 * Store the provided byte array to be written when {@link #writeBufferedRawData()} method 497 * is invoked. 498 * This intermediate step is needed to help in buffering file writes. 499 * 500 * @param data raw data to be written to the file. 501 */ storeRawDataToWrite(byte[] data)502 public void storeRawDataToWrite(byte[] data) { 503 mWriteData = data; 504 } 505 506 /** 507 * Write the stored raw data to the store file. 508 * After the write to file, the mWriteData member is reset. 509 * @throws IOException if an error occurs. The output stream is always closed by the method 510 * even when an exception is encountered. 511 */ writeBufferedRawData()512 public void writeBufferedRawData() throws IOException { 513 if (mWriteData == null) { 514 Log.w(TAG, "No data stored for writing to file: " + mFileName); 515 return; 516 } 517 // Write the data to the atomic file. 518 FileOutputStream out = null; 519 try { 520 out = mAtomicFile.startWrite(); 521 FileUtils.setPermissions(mFileName, FILE_MODE, -1, -1); 522 out.write(mWriteData); 523 mAtomicFile.finishWrite(out); 524 } catch (IOException e) { 525 if (out != null) { 526 mAtomicFile.failWrite(out); 527 } 528 throw e; 529 } 530 // Reset the pending write data after write. 531 mWriteData = null; 532 } 533 } 534 535 /** 536 * Interface to be implemented by a module that contained data in the config store file. 537 * 538 * The module will be responsible for serializing/deserializing their own data. 539 */ 540 public interface StoreData { 541 /** 542 * Serialize a XML data block to the output stream. The |shared| flag indicates if the 543 * output stream is backed by a share store or an user store. 544 * 545 * @param out The output stream to serialize the data to 546 * @param shared Flag indicating if the output stream is backed by a share store or an 547 * user store 548 */ serializeData(XmlSerializer out, boolean shared)549 void serializeData(XmlSerializer out, boolean shared) 550 throws XmlPullParserException, IOException; 551 552 /** 553 * Deserialize a XML data block from the input stream. The |shared| flag indicates if the 554 * input stream is backed by a share store or an user store. When |shared| is set to true, 555 * the shared configuration data will be overwritten by the parsed data. Otherwise, 556 * the user configuration will be overwritten by the parsed data. 557 * 558 * @param in The input stream to read the data from 559 * @param outerTagDepth The depth of the outer tag in the XML document 560 * @Param shared Flag indicating if the input stream is backed by a share store or an 561 * user store 562 */ deserializeData(XmlPullParser in, int outerTagDepth, boolean shared)563 void deserializeData(XmlPullParser in, int outerTagDepth, boolean shared) 564 throws XmlPullParserException, IOException; 565 566 /** 567 * Reset configuration data. The |shared| flag indicates which configuration data to 568 * reset. When |shared| is set to true, the shared configuration data will be reset. 569 * Otherwise, the user configuration data will be reset. 570 */ resetData(boolean shared)571 void resetData(boolean shared); 572 573 /** 574 * Return the name of this store data. The data will be enclosed under this tag in 575 * the XML block. 576 * 577 * @return The name of the store data 578 */ getName()579 String getName(); 580 581 /** 582 * Flag indicating if shared configuration data is supported. 583 * 584 * @return true if shared configuration data is supported 585 */ supportShareData()586 boolean supportShareData(); 587 } 588 } 589