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 static java.lang.Math.toIntExact; 20 21 import android.annotation.IntDef; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.app.AlarmManager; 25 import android.content.Context; 26 import android.os.Environment; 27 import android.os.FileUtils; 28 import android.os.Handler; 29 import android.os.Looper; 30 import android.util.Log; 31 import android.util.SparseArray; 32 import android.util.Xml; 33 34 import com.android.internal.annotations.VisibleForTesting; 35 import com.android.internal.os.AtomicFile; 36 import com.android.internal.util.FastXmlSerializer; 37 import com.android.internal.util.Preconditions; 38 import com.android.server.wifi.util.EncryptedData; 39 import com.android.server.wifi.util.WifiConfigStoreEncryptionUtil; 40 import com.android.server.wifi.util.XmlUtil; 41 42 import org.xmlpull.v1.XmlPullParser; 43 import org.xmlpull.v1.XmlPullParserException; 44 import org.xmlpull.v1.XmlSerializer; 45 46 import java.io.ByteArrayInputStream; 47 import java.io.ByteArrayOutputStream; 48 import java.io.File; 49 import java.io.FileDescriptor; 50 import java.io.FileNotFoundException; 51 import java.io.FileOutputStream; 52 import java.io.IOException; 53 import java.io.PrintWriter; 54 import java.lang.annotation.Retention; 55 import java.lang.annotation.RetentionPolicy; 56 import java.nio.charset.StandardCharsets; 57 import java.util.ArrayList; 58 import java.util.Arrays; 59 import java.util.Collection; 60 import java.util.HashSet; 61 import java.util.List; 62 import java.util.Set; 63 import java.util.stream.Collectors; 64 import java.util.stream.Stream; 65 66 /** 67 * This class provides a mechanism to save data to persistent store files {@link StoreFile}. 68 * Modules can register a {@link StoreData} instance indicating the {@StoreFile} into which they 69 * want to save their data to. 70 * 71 * NOTE: 72 * <li>Modules can register their {@StoreData} using 73 * {@link WifiConfigStore#registerStoreData(StoreData)} directly, but should 74 * use {@link WifiConfigManager#saveToStore(boolean)} for any writes.</li> 75 * <li>{@link WifiConfigManager} controls {@link WifiConfigStore} and initiates read at bootup and 76 * store file changes on user switch.</li> 77 * <li>Not thread safe!</li> 78 */ 79 public class WifiConfigStore { 80 /** 81 * Config store file for general shared store file. 82 */ 83 public static final int STORE_FILE_SHARED_GENERAL = 0; 84 /** 85 * Config store file for general user store file. 86 */ 87 public static final int STORE_FILE_USER_GENERAL = 1; 88 /** 89 * Config store file for network suggestions user store file. 90 */ 91 public static final int STORE_FILE_USER_NETWORK_SUGGESTIONS = 2; 92 93 @IntDef(prefix = { "STORE_FILE_" }, value = { 94 STORE_FILE_SHARED_GENERAL, 95 STORE_FILE_USER_GENERAL, 96 STORE_FILE_USER_NETWORK_SUGGESTIONS 97 }) 98 @Retention(RetentionPolicy.SOURCE) 99 public @interface StoreFileId { } 100 101 private static final String XML_TAG_DOCUMENT_HEADER = "WifiConfigStoreData"; 102 private static final String XML_TAG_VERSION = "Version"; 103 private static final String XML_TAG_HEADER_INTEGRITY = "Integrity"; 104 /** 105 * Current config store data version. This will be incremented for any additions. 106 */ 107 private static final int CURRENT_CONFIG_STORE_DATA_VERSION = 3; 108 /** This list of older versions will be used to restore data from older config store. */ 109 /** 110 * First version of the config store data format. 111 */ 112 public static final int INITIAL_CONFIG_STORE_DATA_VERSION = 1; 113 /** 114 * Second version of the config store data format, introduced: 115 * - Integrity info. 116 */ 117 public static final int INTEGRITY_CONFIG_STORE_DATA_VERSION = 2; 118 /** 119 * Third version of the config store data format, 120 * introduced: 121 * - Encryption of credentials 122 * removed: 123 * - Integrity info. 124 */ 125 public static final int ENCRYPT_CREDENTIALS_CONFIG_STORE_DATA_VERSION = 3; 126 127 @IntDef(suffix = { "_VERSION" }, value = { 128 INITIAL_CONFIG_STORE_DATA_VERSION, 129 INTEGRITY_CONFIG_STORE_DATA_VERSION, 130 ENCRYPT_CREDENTIALS_CONFIG_STORE_DATA_VERSION 131 }) 132 @Retention(RetentionPolicy.SOURCE) 133 public @interface Version { } 134 135 /** 136 * Alarm tag to use for starting alarms for buffering file writes. 137 */ 138 @VisibleForTesting 139 public static final String BUFFERED_WRITE_ALARM_TAG = "WriteBufferAlarm"; 140 /** 141 * Log tag. 142 */ 143 private static final String TAG = "WifiConfigStore"; 144 /** 145 * Directory to store the config store files in. 146 */ 147 private static final String STORE_DIRECTORY_NAME = "wifi"; 148 /** 149 * Time interval for buffering file writes for non-forced writes 150 */ 151 private static final int BUFFERED_WRITE_ALARM_INTERVAL_MS = 10 * 1000; 152 /** 153 * Config store file name for general shared store file. 154 */ 155 private static final String STORE_FILE_NAME_SHARED_GENERAL = "WifiConfigStore.xml"; 156 /** 157 * Config store file name for general user store file. 158 */ 159 private static final String STORE_FILE_NAME_USER_GENERAL = "WifiConfigStore.xml"; 160 /** 161 * Config store file name for network suggestions user store file. 162 */ 163 private static final String STORE_FILE_NAME_USER_NETWORK_SUGGESTIONS = 164 "WifiConfigStoreNetworkSuggestions.xml"; 165 /** 166 * Mapping of Store file Id to Store file names. 167 */ 168 private static final SparseArray<String> STORE_ID_TO_FILE_NAME = 169 new SparseArray<String>() {{ 170 put(STORE_FILE_SHARED_GENERAL, STORE_FILE_NAME_SHARED_GENERAL); 171 put(STORE_FILE_USER_GENERAL, STORE_FILE_NAME_USER_GENERAL); 172 put(STORE_FILE_USER_NETWORK_SUGGESTIONS, STORE_FILE_NAME_USER_NETWORK_SUGGESTIONS); 173 }}; 174 /** 175 * Handler instance to post alarm timeouts to 176 */ 177 private final Handler mEventHandler; 178 /** 179 * Alarm manager instance to start buffer timeout alarms. 180 */ 181 private final AlarmManager mAlarmManager; 182 /** 183 * Clock instance to retrieve timestamps for alarms. 184 */ 185 private final Clock mClock; 186 private final WifiMetrics mWifiMetrics; 187 /** 188 * Shared config store file instance. There is 1 shared store file: 189 * {@link #STORE_FILE_NAME_SHARED_GENERAL}. 190 */ 191 private StoreFile mSharedStore; 192 /** 193 * User specific store file instances. There are 2 user store files: 194 * {@link #STORE_FILE_NAME_USER_GENERAL} & {@link #STORE_FILE_NAME_USER_NETWORK_SUGGESTIONS}. 195 */ 196 private List<StoreFile> mUserStores; 197 /** 198 * Verbose logging flag. 199 */ 200 private boolean mVerboseLoggingEnabled = false; 201 /** 202 * Flag to indicate if there is a buffered write pending. 203 */ 204 private boolean mBufferedWritePending = false; 205 /** 206 * Alarm listener for flushing out any buffered writes. 207 */ 208 private final AlarmManager.OnAlarmListener mBufferedWriteListener = 209 new AlarmManager.OnAlarmListener() { 210 public void onAlarm() { 211 try { 212 writeBufferedData(); 213 } catch (IOException e) { 214 Log.wtf(TAG, "Buffered write failed", e); 215 } 216 } 217 }; 218 219 /** 220 * List of data containers. 221 */ 222 private final List<StoreData> mStoreDataList; 223 224 /** 225 * Create a new instance of WifiConfigStore. 226 * Note: The store file instances have been made inputs to this class to ease unit-testing. 227 * 228 * @param context context to use for retrieving the alarm manager. 229 * @param looper looper instance to post alarm timeouts to. 230 * @param clock clock instance to retrieve timestamps for alarms. 231 * @param wifiMetrics Metrics instance. 232 * @param sharedStore StoreFile instance pointing to the shared store file. This should 233 * be retrieved using {@link #createSharedFile()} method. 234 */ WifiConfigStore(Context context, Looper looper, Clock clock, WifiMetrics wifiMetrics, StoreFile sharedStore)235 public WifiConfigStore(Context context, Looper looper, Clock clock, WifiMetrics wifiMetrics, 236 StoreFile sharedStore) { 237 238 mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 239 mEventHandler = new Handler(looper); 240 mClock = clock; 241 mWifiMetrics = wifiMetrics; 242 mStoreDataList = new ArrayList<>(); 243 244 // Initialize the store files. 245 mSharedStore = sharedStore; 246 // The user store is initialized to null, this will be set when the user unlocks and 247 // CE storage is accessible via |switchUserStoresAndRead|. 248 mUserStores = null; 249 } 250 251 /** 252 * Set the user store files. 253 * (Useful for mocking in unit tests). 254 * @param userStores List of {@link StoreFile} created using {@link #createUserFiles(int)}. 255 */ setUserStores(@onNull List<StoreFile> userStores)256 public void setUserStores(@NonNull List<StoreFile> userStores) { 257 Preconditions.checkNotNull(userStores); 258 mUserStores = userStores; 259 } 260 261 /** 262 * Register a {@link StoreData} to read/write data from/to a store. A {@link StoreData} is 263 * responsible for a block of data in the store file, and provides serialization/deserialization 264 * functions for those data. 265 * 266 * @param storeData The store data to be registered to the config store 267 * @return true if registered successfully, false if the store file name is not valid. 268 */ registerStoreData(@onNull StoreData storeData)269 public boolean registerStoreData(@NonNull StoreData storeData) { 270 if (storeData == null) { 271 Log.e(TAG, "Unable to register null store data"); 272 return false; 273 } 274 int storeFileId = storeData.getStoreFileId(); 275 if (STORE_ID_TO_FILE_NAME.get(storeFileId) == null) { 276 Log.e(TAG, "Invalid shared store file specified" + storeFileId); 277 return false; 278 } 279 mStoreDataList.add(storeData); 280 return true; 281 } 282 283 /** 284 * Helper method to create a store file instance for either the shared store or user store. 285 * Note: The method creates the store directory if not already present. This may be needed for 286 * user store files. 287 * 288 * @param storeBaseDir Base directory under which the store file is to be stored. The store file 289 * will be at <storeBaseDir>/wifi/WifiConfigStore.xml. 290 * @param fileId Identifier for the file. See {@link StoreFileId}. 291 * @param shouldEncryptCredentials Whether to encrypt credentials or not. 292 * @return new instance of the store file or null if the directory cannot be created. 293 */ createFile(File storeBaseDir, @StoreFileId int fileId, boolean shouldEncryptCredentials)294 private static @Nullable StoreFile createFile(File storeBaseDir, @StoreFileId int fileId, 295 boolean shouldEncryptCredentials) { 296 File storeDir = new File(storeBaseDir, STORE_DIRECTORY_NAME); 297 if (!storeDir.exists()) { 298 if (!storeDir.mkdir()) { 299 Log.w(TAG, "Could not create store directory " + storeDir); 300 return null; 301 } 302 } 303 File file = new File(storeDir, STORE_ID_TO_FILE_NAME.get(fileId)); 304 WifiConfigStoreEncryptionUtil encryptionUtil = null; 305 if (shouldEncryptCredentials) { 306 encryptionUtil = new WifiConfigStoreEncryptionUtil(file.getName()); 307 } 308 return new StoreFile(file, fileId, encryptionUtil); 309 } 310 311 /** 312 * Create a new instance of the shared store file. 313 * 314 * @param shouldEncryptCredentials Whether to encrypt credentials or not. 315 * @return new instance of the store file or null if the directory cannot be created. 316 */ createSharedFile(boolean shouldEncryptCredentials)317 public static @Nullable StoreFile createSharedFile(boolean shouldEncryptCredentials) { 318 return createFile(Environment.getDataMiscDirectory(), STORE_FILE_SHARED_GENERAL, 319 shouldEncryptCredentials); 320 } 321 322 /** 323 * Create new instances of the user specific store files. 324 * The user store file is inside the user's encrypted data directory. 325 * 326 * @param userId userId corresponding to the currently logged-in user. 327 * @param shouldEncryptCredentials Whether to encrypt credentials or not. 328 * @return List of new instances of the store files created or null if the directory cannot be 329 * created. 330 */ createUserFiles(int userId, boolean shouldEncryptCredentials)331 public static @Nullable List<StoreFile> createUserFiles(int userId, 332 boolean shouldEncryptCredentials) { 333 List<StoreFile> storeFiles = new ArrayList<>(); 334 for (int fileId : Arrays.asList( 335 STORE_FILE_USER_GENERAL, STORE_FILE_USER_NETWORK_SUGGESTIONS)) { 336 StoreFile storeFile = 337 createFile(Environment.getDataMiscCeDirectory(userId), fileId, 338 shouldEncryptCredentials); 339 if (storeFile == null) { 340 return null; 341 } 342 storeFiles.add(storeFile); 343 } 344 return storeFiles; 345 } 346 347 /** 348 * Enable verbose logging. 349 */ enableVerboseLogging(boolean verbose)350 public void enableVerboseLogging(boolean verbose) { 351 mVerboseLoggingEnabled = verbose; 352 } 353 354 /** 355 * API to check if any of the store files are present on the device. This can be used 356 * to detect if the device needs to perform data migration from legacy stores. 357 * 358 * @return true if any of the store file is present, false otherwise. 359 */ areStoresPresent()360 public boolean areStoresPresent() { 361 // Checking for the shared store file existence is sufficient since this is guaranteed 362 // to be present on migrated devices. 363 return mSharedStore.exists(); 364 } 365 366 /** 367 * Retrieve the list of {@link StoreData} instances registered for the provided 368 * {@link StoreFile}. 369 */ retrieveStoreDataListForStoreFile(@onNull StoreFile storeFile)370 private List<StoreData> retrieveStoreDataListForStoreFile(@NonNull StoreFile storeFile) { 371 return mStoreDataList 372 .stream() 373 .filter(s -> s.getStoreFileId() == storeFile.mFileId) 374 .collect(Collectors.toList()); 375 } 376 377 /** 378 * Check if any of the provided list of {@link StoreData} instances registered 379 * for the provided {@link StoreFile }have indicated that they have new data to serialize. 380 */ hasNewDataToSerialize(@onNull StoreFile storeFile)381 private boolean hasNewDataToSerialize(@NonNull StoreFile storeFile) { 382 List<StoreData> storeDataList = retrieveStoreDataListForStoreFile(storeFile); 383 return storeDataList.stream().anyMatch(s -> s.hasNewDataToSerialize()); 384 } 385 386 /** 387 * API to write the data provided by registered store data to config stores. 388 * The method writes the user specific configurations to user specific config store and the 389 * shared configurations to shared config store. 390 * 391 * @param forceSync boolean to force write the config stores now. if false, the writes are 392 * buffered and written after the configured interval. 393 */ write(boolean forceSync)394 public void write(boolean forceSync) 395 throws XmlPullParserException, IOException { 396 boolean hasAnyNewData = false; 397 // Serialize the provided data and send it to the respective stores. The actual write will 398 // be performed later depending on the |forceSync| flag . 399 if (hasNewDataToSerialize(mSharedStore)) { 400 byte[] sharedDataBytes = serializeData(mSharedStore); 401 mSharedStore.storeRawDataToWrite(sharedDataBytes); 402 hasAnyNewData = true; 403 } 404 if (mUserStores != null) { 405 for (StoreFile userStoreFile : mUserStores) { 406 if (hasNewDataToSerialize(userStoreFile)) { 407 byte[] userDataBytes = serializeData(userStoreFile); 408 userStoreFile.storeRawDataToWrite(userDataBytes); 409 hasAnyNewData = true; 410 } 411 } 412 } 413 414 if (hasAnyNewData) { 415 // Every write provides a new snapshot to be persisted, so |forceSync| flag overrides 416 // any pending buffer writes. 417 if (forceSync) { 418 writeBufferedData(); 419 } else { 420 startBufferedWriteAlarm(); 421 } 422 } else if (forceSync && mBufferedWritePending) { 423 // no new data to write, but there is a pending buffered write. So, |forceSync| should 424 // flush that out. 425 writeBufferedData(); 426 } 427 } 428 429 /** 430 * Serialize all the data from all the {@link StoreData} clients registered for the provided 431 * {@link StoreFile}. 432 * 433 * This method also computes the integrity of the data being written and serializes the computed 434 * {@link EncryptedData} to the output. 435 * 436 * @param storeFile StoreFile that we want to write to. 437 * @return byte[] of serialized bytes 438 * @throws XmlPullParserException 439 * @throws IOException 440 */ serializeData(@onNull StoreFile storeFile)441 private byte[] serializeData(@NonNull StoreFile storeFile) 442 throws XmlPullParserException, IOException { 443 List<StoreData> storeDataList = retrieveStoreDataListForStoreFile(storeFile); 444 445 final XmlSerializer out = new FastXmlSerializer(); 446 final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 447 out.setOutput(outputStream, StandardCharsets.UTF_8.name()); 448 449 // First XML header. 450 XmlUtil.writeDocumentStart(out, XML_TAG_DOCUMENT_HEADER); 451 // Next version. 452 XmlUtil.writeNextValue(out, XML_TAG_VERSION, CURRENT_CONFIG_STORE_DATA_VERSION); 453 for (StoreData storeData : storeDataList) { 454 String tag = storeData.getName(); 455 XmlUtil.writeNextSectionStart(out, tag); 456 storeData.serializeData(out, storeFile.getEncryptionUtil()); 457 XmlUtil.writeNextSectionEnd(out, tag); 458 } 459 XmlUtil.writeDocumentEnd(out, XML_TAG_DOCUMENT_HEADER); 460 return outputStream.toByteArray(); 461 } 462 463 /** 464 * Helper method to start a buffered write alarm if one doesn't already exist. 465 */ startBufferedWriteAlarm()466 private void startBufferedWriteAlarm() { 467 if (!mBufferedWritePending) { 468 mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 469 mClock.getElapsedSinceBootMillis() + BUFFERED_WRITE_ALARM_INTERVAL_MS, 470 BUFFERED_WRITE_ALARM_TAG, mBufferedWriteListener, mEventHandler); 471 mBufferedWritePending = true; 472 } 473 } 474 475 /** 476 * Helper method to stop a buffered write alarm if one exists. 477 */ stopBufferedWriteAlarm()478 private void stopBufferedWriteAlarm() { 479 if (mBufferedWritePending) { 480 mAlarmManager.cancel(mBufferedWriteListener); 481 mBufferedWritePending = false; 482 } 483 } 484 485 /** 486 * Helper method to actually perform the writes to the file. This flushes out any write data 487 * being buffered in the respective stores and cancels any pending buffer write alarms. 488 */ writeBufferedData()489 private void writeBufferedData() throws IOException { 490 stopBufferedWriteAlarm(); 491 492 long writeStartTime = mClock.getElapsedSinceBootMillis(); 493 mSharedStore.writeBufferedRawData(); 494 if (mUserStores != null) { 495 for (StoreFile userStoreFile : mUserStores) { 496 userStoreFile.writeBufferedRawData(); 497 } 498 } 499 long writeTime = mClock.getElapsedSinceBootMillis() - writeStartTime; 500 try { 501 mWifiMetrics.noteWifiConfigStoreWriteDuration(toIntExact(writeTime)); 502 } catch (ArithmeticException e) { 503 // Silently ignore on any overflow errors. 504 } 505 Log.d(TAG, "Writing to stores completed in " + writeTime + " ms."); 506 } 507 508 /** 509 * API to read the store data from the config stores. 510 * The method reads the user specific configurations from user specific config store and the 511 * shared configurations from the shared config store. 512 */ read()513 public void read() throws XmlPullParserException, IOException { 514 // Reset both share and user store data. 515 resetStoreData(mSharedStore); 516 if (mUserStores != null) { 517 for (StoreFile userStoreFile : mUserStores) { 518 resetStoreData(userStoreFile); 519 } 520 } 521 522 long readStartTime = mClock.getElapsedSinceBootMillis(); 523 byte[] sharedDataBytes = mSharedStore.readRawData(); 524 deserializeData(sharedDataBytes, mSharedStore); 525 if (mUserStores != null) { 526 for (StoreFile userStoreFile : mUserStores) { 527 byte[] userDataBytes = userStoreFile.readRawData(); 528 deserializeData(userDataBytes, userStoreFile); 529 } 530 } 531 long readTime = mClock.getElapsedSinceBootMillis() - readStartTime; 532 try { 533 mWifiMetrics.noteWifiConfigStoreReadDuration(toIntExact(readTime)); 534 } catch (ArithmeticException e) { 535 // Silently ignore on any overflow errors. 536 } 537 Log.d(TAG, "Reading from all stores completed in " + readTime + " ms."); 538 } 539 540 /** 541 * Handles a user switch. This method changes the user specific store files and reads from the 542 * new user's store files. 543 * 544 * @param userStores List of {@link StoreFile} created using {@link #createUserFiles(int)}. 545 */ switchUserStoresAndRead(@onNull List<StoreFile> userStores)546 public void switchUserStoresAndRead(@NonNull List<StoreFile> userStores) 547 throws XmlPullParserException, IOException { 548 Preconditions.checkNotNull(userStores); 549 // Reset user store data. 550 if (mUserStores != null) { 551 for (StoreFile userStoreFile : mUserStores) { 552 resetStoreData(userStoreFile); 553 } 554 } 555 556 // Stop any pending buffered writes, if any. 557 stopBufferedWriteAlarm(); 558 mUserStores = userStores; 559 560 // Now read from the user store file. 561 long readStartTime = mClock.getElapsedSinceBootMillis(); 562 for (StoreFile userStoreFile : mUserStores) { 563 byte[] userDataBytes = userStoreFile.readRawData(); 564 deserializeData(userDataBytes, userStoreFile); 565 } 566 long readTime = mClock.getElapsedSinceBootMillis() - readStartTime; 567 mWifiMetrics.noteWifiConfigStoreReadDuration(toIntExact(readTime)); 568 Log.d(TAG, "Reading from user stores completed in " + readTime + " ms."); 569 } 570 571 /** 572 * Reset data for all {@link StoreData} instances registered for this {@link StoreFile}. 573 */ resetStoreData(@onNull StoreFile storeFile)574 private void resetStoreData(@NonNull StoreFile storeFile) { 575 for (StoreData storeData: retrieveStoreDataListForStoreFile(storeFile)) { 576 storeData.resetData(); 577 } 578 } 579 580 // Inform all the provided store data clients that there is nothing in the store for them. indicateNoDataForStoreDatas(Collection<StoreData> storeDataSet, @Version int version, @NonNull WifiConfigStoreEncryptionUtil encryptionUtil)581 private void indicateNoDataForStoreDatas(Collection<StoreData> storeDataSet, 582 @Version int version, @NonNull WifiConfigStoreEncryptionUtil encryptionUtil) 583 throws XmlPullParserException, IOException { 584 for (StoreData storeData : storeDataSet) { 585 storeData.deserializeData(null, 0, version, encryptionUtil); 586 } 587 } 588 589 /** 590 * Deserialize data from a {@link StoreFile} for all {@link StoreData} instances registered. 591 * 592 * This method also computes the integrity of the incoming |dataBytes| and compare with 593 * {@link EncryptedData} parsed from |dataBytes|. If the integrity check fails, the data 594 * is discarded. 595 * 596 * @param dataBytes The data to parse 597 * @param storeFile StoreFile that we read from. Will be used to retrieve the list of clients 598 * who have data to deserialize from this file. 599 * 600 * @throws XmlPullParserException 601 * @throws IOException 602 */ deserializeData(@onNull byte[] dataBytes, @NonNull StoreFile storeFile)603 private void deserializeData(@NonNull byte[] dataBytes, @NonNull StoreFile storeFile) 604 throws XmlPullParserException, IOException { 605 List<StoreData> storeDataList = retrieveStoreDataListForStoreFile(storeFile); 606 if (dataBytes == null) { 607 indicateNoDataForStoreDatas(storeDataList, -1 /* unknown */, 608 storeFile.getEncryptionUtil()); 609 return; 610 } 611 final XmlPullParser in = Xml.newPullParser(); 612 final ByteArrayInputStream inputStream = new ByteArrayInputStream(dataBytes); 613 in.setInput(inputStream, StandardCharsets.UTF_8.name()); 614 615 // Start parsing the XML stream. 616 int rootTagDepth = in.getDepth() + 1; 617 XmlUtil.gotoDocumentStart(in, XML_TAG_DOCUMENT_HEADER); 618 619 @Version int version = parseVersionFromXml(in); 620 // Version 2 contains the now unused integrity data, parse & then discard the information. 621 if (version == INTEGRITY_CONFIG_STORE_DATA_VERSION) { 622 parseAndDiscardIntegrityDataFromXml(in, rootTagDepth); 623 } 624 625 String[] headerName = new String[1]; 626 Set<StoreData> storeDatasInvoked = new HashSet<>(); 627 while (XmlUtil.gotoNextSectionOrEnd(in, headerName, rootTagDepth)) { 628 // There can only be 1 store data matching the tag (O indicates a fatal 629 // error). 630 StoreData storeData = storeDataList.stream() 631 .filter(s -> s.getName().equals(headerName[0])) 632 .findAny() 633 .orElse(null); 634 if (storeData == null) { 635 throw new XmlPullParserException("Unknown store data: " + headerName[0] 636 + ". List of store data: " + storeDataList); 637 } 638 storeData.deserializeData(in, rootTagDepth + 1, version, 639 storeFile.getEncryptionUtil()); 640 storeDatasInvoked.add(storeData); 641 } 642 // Inform all the other registered store data clients that there is nothing in the store 643 // for them. 644 Set<StoreData> storeDatasNotInvoked = new HashSet<>(storeDataList); 645 storeDatasNotInvoked.removeAll(storeDatasInvoked); 646 indicateNoDataForStoreDatas(storeDatasNotInvoked, version, storeFile.getEncryptionUtil()); 647 } 648 649 /** 650 * Parse the version from the XML stream. 651 * This is used for both the shared and user config store data. 652 * 653 * @param in XmlPullParser instance pointing to the XML stream. 654 * @return version number retrieved from the Xml stream. 655 */ parseVersionFromXml(XmlPullParser in)656 private static @Version int parseVersionFromXml(XmlPullParser in) 657 throws XmlPullParserException, IOException { 658 int version = (int) XmlUtil.readNextValueWithName(in, XML_TAG_VERSION); 659 if (version < INITIAL_CONFIG_STORE_DATA_VERSION 660 || version > CURRENT_CONFIG_STORE_DATA_VERSION) { 661 throw new XmlPullParserException("Invalid version of data: " + version); 662 } 663 return version; 664 } 665 666 /** 667 * Parse the integrity data structure from the XML stream and discard it. 668 * 669 * @param in XmlPullParser instance pointing to the XML stream. 670 * @param outerTagDepth Outer tag depth. 671 */ parseAndDiscardIntegrityDataFromXml(XmlPullParser in, int outerTagDepth)672 private static void parseAndDiscardIntegrityDataFromXml(XmlPullParser in, int outerTagDepth) 673 throws XmlPullParserException, IOException { 674 XmlUtil.gotoNextSectionWithName(in, XML_TAG_HEADER_INTEGRITY, outerTagDepth); 675 XmlUtil.EncryptedDataXmlUtil.parseFromXml(in, outerTagDepth + 1); 676 } 677 678 /** 679 * Dump the local log buffer and other internal state of WifiConfigManager. 680 */ dump(FileDescriptor fd, PrintWriter pw, String[] args)681 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 682 pw.println("Dump of WifiConfigStore"); 683 pw.println("WifiConfigStore - Store File Begin ----"); 684 Stream.of(Arrays.asList(mSharedStore), mUserStores) 685 .flatMap(List::stream) 686 .forEach((storeFile) -> { 687 pw.print("Name: " + storeFile.mFileName); 688 pw.println(", Credentials encrypted: " + storeFile.getEncryptionUtil() != null); 689 }); 690 pw.println("WifiConfigStore - Store Data Begin ----"); 691 for (StoreData storeData : mStoreDataList) { 692 pw.print("StoreData =>"); 693 pw.print(" "); 694 pw.print("Name: " + storeData.getName()); 695 pw.print(", "); 696 pw.print("File Id: " + storeData.getStoreFileId()); 697 pw.print(", "); 698 pw.println("File Name: " + STORE_ID_TO_FILE_NAME.get(storeData.getStoreFileId())); 699 } 700 pw.println("WifiConfigStore - Store Data End ----"); 701 } 702 703 /** 704 * Class to encapsulate all file writes. This is a wrapper over {@link AtomicFile} to write/read 705 * raw data from the persistent file with integrity. This class provides helper methods to 706 * read/write the entire file into a byte array. 707 * This helps to separate out the processing, parsing, and integrity checking from the actual 708 * file writing. 709 */ 710 public static class StoreFile { 711 /** 712 * File permissions to lock down the file. 713 */ 714 private static final int FILE_MODE = 0600; 715 /** 716 * The store file to be written to. 717 */ 718 private final AtomicFile mAtomicFile; 719 /** 720 * This is an intermediate buffer to store the data to be written. 721 */ 722 private byte[] mWriteData; 723 /** 724 * Store the file name for setting the file permissions/logging purposes. 725 */ 726 private final String mFileName; 727 /** 728 * {@link StoreFileId} Type of store file. 729 */ 730 private final @StoreFileId int mFileId; 731 /** 732 * Integrity checking for the store file. 733 */ 734 private final WifiConfigStoreEncryptionUtil mEncryptionUtil; 735 StoreFile(File file, @StoreFileId int fileId, @Nullable WifiConfigStoreEncryptionUtil encryptionUtil)736 public StoreFile(File file, @StoreFileId int fileId, 737 @Nullable WifiConfigStoreEncryptionUtil encryptionUtil) { 738 mAtomicFile = new AtomicFile(file); 739 mFileName = file.getAbsolutePath(); 740 mFileId = fileId; 741 mEncryptionUtil = encryptionUtil; 742 } 743 744 /** 745 * Returns whether the store file already exists on disk or not. 746 * 747 * @return true if it exists, false otherwise. 748 */ exists()749 public boolean exists() { 750 return mAtomicFile.exists(); 751 } 752 753 /** 754 * @return Returns the encryption util used for this store file. 755 */ getEncryptionUtil()756 public @Nullable WifiConfigStoreEncryptionUtil getEncryptionUtil() { 757 return mEncryptionUtil; 758 } 759 760 /** 761 * Read the entire raw data from the store file and return in a byte array. 762 * 763 * @return raw data read from the file or null if the file is not found or the data has 764 * been altered. 765 * @throws IOException if an error occurs. The input stream is always closed by the method 766 * even when an exception is encountered. 767 */ readRawData()768 public byte[] readRawData() throws IOException { 769 byte[] bytes = null; 770 try { 771 bytes = mAtomicFile.readFully(); 772 } catch (FileNotFoundException e) { 773 return null; 774 } 775 return bytes; 776 } 777 778 /** 779 * Store the provided byte array to be written when {@link #writeBufferedRawData()} method 780 * is invoked. 781 * This intermediate step is needed to help in buffering file writes. 782 * 783 * @param data raw data to be written to the file. 784 */ storeRawDataToWrite(byte[] data)785 public void storeRawDataToWrite(byte[] data) { 786 mWriteData = data; 787 } 788 789 /** 790 * Write the stored raw data to the store file. 791 * After the write to file, the mWriteData member is reset. 792 * @throws IOException if an error occurs. The output stream is always closed by the method 793 * even when an exception is encountered. 794 */ writeBufferedRawData()795 public void writeBufferedRawData() throws IOException { 796 if (mWriteData == null) return; // No data to write for this file. 797 // Write the data to the atomic file. 798 FileOutputStream out = null; 799 try { 800 out = mAtomicFile.startWrite(); 801 FileUtils.setPermissions(mFileName, FILE_MODE, -1, -1); 802 out.write(mWriteData); 803 mAtomicFile.finishWrite(out); 804 } catch (IOException e) { 805 if (out != null) { 806 mAtomicFile.failWrite(out); 807 } 808 throw e; 809 } 810 // Reset the pending write data after write. 811 mWriteData = null; 812 } 813 } 814 815 /** 816 * Interface to be implemented by a module that contained data in the config store file. 817 * 818 * The module will be responsible for serializing/deserializing their own data. 819 * Whenever {@link WifiConfigStore#read()} is invoked, all registered StoreData instances will 820 * be notified that a read was performed via {@link StoreData#deserializeData( 821 * XmlPullParser, int)} regardless of whether there is any data for them or not in the 822 * store file. 823 * 824 * Note: StoreData clients that need a config store read to kick-off operations should wait 825 * for the {@link StoreData#deserializeData(XmlPullParser, int)} invocation. 826 */ 827 public interface StoreData { 828 /** 829 * Serialize a XML data block to the output stream. 830 * 831 * @param out The output stream to serialize the data to 832 * @param encryptionUtil Utility to help encrypt any credential data. 833 */ serializeData(XmlSerializer out, @Nullable WifiConfigStoreEncryptionUtil encryptionUtil)834 void serializeData(XmlSerializer out, 835 @Nullable WifiConfigStoreEncryptionUtil encryptionUtil) 836 throws XmlPullParserException, IOException; 837 838 /** 839 * Deserialize a XML data block from the input stream. 840 * 841 * @param in The input stream to read the data from. This could be null if there is 842 * nothing in the store. 843 * @param outerTagDepth The depth of the outer tag in the XML document 844 * @param version Version of config store file. 845 * @param encryptionUtil Utility to help decrypt any credential data. 846 * 847 * Note: This will be invoked every time a store file is read, even if there is nothing 848 * in the store for them. 849 */ deserializeData(@ullable XmlPullParser in, int outerTagDepth, @Version int version, @Nullable WifiConfigStoreEncryptionUtil encryptionUtil)850 void deserializeData(@Nullable XmlPullParser in, int outerTagDepth, @Version int version, 851 @Nullable WifiConfigStoreEncryptionUtil encryptionUtil) 852 throws XmlPullParserException, IOException; 853 854 /** 855 * Reset configuration data. 856 */ resetData()857 void resetData(); 858 859 /** 860 * Check if there is any new data to persist from the last write. 861 * 862 * @return true if the module has new data to persist, false otherwise. 863 */ hasNewDataToSerialize()864 boolean hasNewDataToSerialize(); 865 866 /** 867 * Return the name of this store data. The data will be enclosed under this tag in 868 * the XML block. 869 * 870 * @return The name of the store data 871 */ getName()872 String getName(); 873 874 /** 875 * File Id where this data needs to be written to. 876 * This should be one of {@link #STORE_FILE_SHARED_GENERAL}, 877 * {@link #STORE_FILE_USER_GENERAL} or 878 * {@link #STORE_FILE_USER_NETWORK_SUGGESTIONS}. 879 * 880 * Note: For most uses, the shared or user general store is sufficient. Creating and 881 * managing store files are expensive. Only use specific store files if you have a large 882 * amount of data which may not need to be persisted frequently (or at least not as 883 * frequently as the general store). 884 * @return Id of the file where this data needs to be persisted. 885 */ getStoreFileId()886 @StoreFileId int getStoreFileId(); 887 } 888 } 889