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