/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.wifi; import static java.lang.Math.toIntExact; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AlarmManager; import android.content.Context; import android.net.wifi.WifiMigration; import android.os.Handler; import android.os.UserHandle; import android.util.AtomicFile; import android.util.Log; import android.util.SparseArray; import android.util.Xml; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.FastXmlSerializer; import com.android.internal.util.Preconditions; import com.android.server.wifi.util.EncryptedData; import com.android.server.wifi.util.Environment; import com.android.server.wifi.util.FileUtils; import com.android.server.wifi.util.WifiConfigStoreEncryptionUtil; import com.android.server.wifi.util.XmlUtil; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; /** * This class provides a mechanism to save data to persistent store files {@link StoreFile}. * Modules can register a {@link StoreData} instance indicating the {@StoreFile} into which they * want to save their data to. * * NOTE: *
  • Modules can register their {@StoreData} using * {@link WifiConfigStore#registerStoreData(StoreData)} directly, but should * use {@link WifiConfigManager#saveToStore(boolean)} for any writes.
  • *
  • {@link WifiConfigManager} controls {@link WifiConfigStore} and initiates read at bootup and * store file changes on user switch.
  • *
  • Not thread safe!
  • */ public class WifiConfigStore { /** * Config store file for general shared store file. */ public static final int STORE_FILE_SHARED_GENERAL = 0; /** * Config store file for softap shared store file. */ public static final int STORE_FILE_SHARED_SOFTAP = 1; /** * Config store file for general user store file. */ public static final int STORE_FILE_USER_GENERAL = 2; /** * Config store file for network suggestions user store file. */ public static final int STORE_FILE_USER_NETWORK_SUGGESTIONS = 3; @IntDef(prefix = { "STORE_FILE_" }, value = { STORE_FILE_SHARED_GENERAL, STORE_FILE_SHARED_SOFTAP, STORE_FILE_USER_GENERAL, STORE_FILE_USER_NETWORK_SUGGESTIONS }) @Retention(RetentionPolicy.SOURCE) public @interface StoreFileId { } private static final String XML_TAG_DOCUMENT_HEADER = "WifiConfigStoreData"; private static final String XML_TAG_VERSION = "Version"; private static final String XML_TAG_HEADER_INTEGRITY = "Integrity"; /** * Current config store data version. This will be incremented for any additions. */ private static final int CURRENT_CONFIG_STORE_DATA_VERSION = 3; /** This list of older versions will be used to restore data from older config store. */ /** * First version of the config store data format. */ public static final int INITIAL_CONFIG_STORE_DATA_VERSION = 1; /** * Second version of the config store data format, introduced: * - Integrity info. */ public static final int INTEGRITY_CONFIG_STORE_DATA_VERSION = 2; /** * Third version of the config store data format, * introduced: * - Encryption of credentials * removed: * - Integrity info. */ public static final int ENCRYPT_CREDENTIALS_CONFIG_STORE_DATA_VERSION = 3; @IntDef(suffix = { "_VERSION" }, value = { INITIAL_CONFIG_STORE_DATA_VERSION, INTEGRITY_CONFIG_STORE_DATA_VERSION, ENCRYPT_CREDENTIALS_CONFIG_STORE_DATA_VERSION }) @Retention(RetentionPolicy.SOURCE) public @interface Version { } /** * Alarm tag to use for starting alarms for buffering file writes. */ @VisibleForTesting public static final String BUFFERED_WRITE_ALARM_TAG = "WriteBufferAlarm"; /** * Log tag. */ private static final String TAG = "WifiConfigStore"; /** * Time interval for buffering file writes for non-forced writes */ private static final int BUFFERED_WRITE_ALARM_INTERVAL_MS = 10 * 1000; /** * Config store file name for general shared store file. */ private static final String STORE_FILE_NAME_SHARED_GENERAL = "WifiConfigStore.xml"; /** * Config store file name for SoftAp shared store file. */ private static final String STORE_FILE_NAME_SHARED_SOFTAP = "WifiConfigStoreSoftAp.xml"; /** * Config store file name for general user store file. */ private static final String STORE_FILE_NAME_USER_GENERAL = "WifiConfigStore.xml"; /** * Config store file name for network suggestions user store file. */ private static final String STORE_FILE_NAME_USER_NETWORK_SUGGESTIONS = "WifiConfigStoreNetworkSuggestions.xml"; /** * Mapping of Store file Id to Store file names. */ private static final SparseArray STORE_ID_TO_FILE_NAME = new SparseArray() {{ put(STORE_FILE_SHARED_GENERAL, STORE_FILE_NAME_SHARED_GENERAL); put(STORE_FILE_SHARED_SOFTAP, STORE_FILE_NAME_SHARED_SOFTAP); put(STORE_FILE_USER_GENERAL, STORE_FILE_NAME_USER_GENERAL); put(STORE_FILE_USER_NETWORK_SUGGESTIONS, STORE_FILE_NAME_USER_NETWORK_SUGGESTIONS); }}; /** * Handler instance to post alarm timeouts to */ private final Handler mEventHandler; /** * Alarm manager instance to start buffer timeout alarms. */ private final AlarmManager mAlarmManager; /** * Clock instance to retrieve timestamps for alarms. */ private final Clock mClock; private final WifiMetrics mWifiMetrics; /** * Shared config store file instance. There are 2 shared store files: * {@link #STORE_FILE_NAME_SHARED_GENERAL} & {@link #STORE_FILE_NAME_SHARED_SOFTAP}. */ private final List mSharedStores; /** * User specific store file instances. There are 2 user store files: * {@link #STORE_FILE_NAME_USER_GENERAL} & {@link #STORE_FILE_NAME_USER_NETWORK_SUGGESTIONS}. */ private List mUserStores; /** * Verbose logging flag. */ private boolean mVerboseLoggingEnabled = false; /** * Flag to indicate if there is a buffered write pending. */ private boolean mBufferedWritePending = false; /** * Alarm listener for flushing out any buffered writes. */ private final AlarmManager.OnAlarmListener mBufferedWriteListener = new AlarmManager.OnAlarmListener() { public void onAlarm() { try { writeBufferedData(); } catch (IOException e) { Log.wtf(TAG, "Buffered write failed", e); } } }; /** * List of data containers. */ private final List mStoreDataList; /** * Create a new instance of WifiConfigStore. * Note: The store file instances have been made inputs to this class to ease unit-testing. * * @param context context to use for retrieving the alarm manager. * @param handler handler instance to post alarm timeouts to. * @param clock clock instance to retrieve timestamps for alarms. * @param wifiMetrics Metrics instance. * @param sharedStores List of {@link StoreFile} instances pointing to the shared store files. * This should be retrieved using {@link #createSharedFiles(boolean)} * method. */ public WifiConfigStore(Context context, Handler handler, Clock clock, WifiMetrics wifiMetrics, List sharedStores) { mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); mEventHandler = handler; mClock = clock; mWifiMetrics = wifiMetrics; mStoreDataList = new ArrayList<>(); // Initialize the store files. mSharedStores = sharedStores; // The user store is initialized to null, this will be set when the user unlocks and // CE storage is accessible via |switchUserStoresAndRead|. mUserStores = null; } /** * Set the user store files. * (Useful for mocking in unit tests). * @param userStores List of {@link StoreFile} created using * {@link #createUserFiles(int, boolean)}. */ public void setUserStores(@NonNull List userStores) { Preconditions.checkNotNull(userStores); mUserStores = userStores; } /** * Register a {@link StoreData} to read/write data from/to a store. A {@link StoreData} is * responsible for a block of data in the store file, and provides serialization/deserialization * functions for those data. * * @param storeData The store data to be registered to the config store * @return true if registered successfully, false if the store file name is not valid. */ public boolean registerStoreData(@NonNull StoreData storeData) { if (storeData == null) { Log.e(TAG, "Unable to register null store data"); return false; } int storeFileId = storeData.getStoreFileId(); if (STORE_ID_TO_FILE_NAME.get(storeFileId) == null) { Log.e(TAG, "Invalid shared store file specified" + storeFileId); return false; } mStoreDataList.add(storeData); return true; } /** * Helper method to create a store file instance for either the shared store or user store. * Note: The method creates the store directory if not already present. This may be needed for * user store files. * * @param storeDir Base directory under which the store file is to be stored. The store file * will be at /WifiConfigStore.xml. * @param fileId Identifier for the file. See {@link StoreFileId}. * @param userHandle User handle. Meaningful only for user specific store files. * @param shouldEncryptCredentials Whether to encrypt credentials or not. * @return new instance of the store file or null if the directory cannot be created. */ private static @Nullable StoreFile createFile(@NonNull File storeDir, @StoreFileId int fileId, UserHandle userHandle, boolean shouldEncryptCredentials) { if (!storeDir.exists()) { if (!storeDir.mkdir()) { Log.w(TAG, "Could not create store directory " + storeDir); return null; } } File file = new File(storeDir, STORE_ID_TO_FILE_NAME.get(fileId)); WifiConfigStoreEncryptionUtil encryptionUtil = null; if (shouldEncryptCredentials) { encryptionUtil = new WifiConfigStoreEncryptionUtil(file.getName()); } return new StoreFile(file, fileId, userHandle, encryptionUtil); } private static @Nullable List createFiles(File storeDir, List storeFileIds, UserHandle userHandle, boolean shouldEncryptCredentials) { List storeFiles = new ArrayList<>(); for (int fileId : storeFileIds) { StoreFile storeFile = createFile(storeDir, fileId, userHandle, shouldEncryptCredentials); if (storeFile == null) { return null; } storeFiles.add(storeFile); } return storeFiles; } /** * Create a new instance of the shared store file. * * @param shouldEncryptCredentials Whether to encrypt credentials or not. * @return new instance of the store file or null if the directory cannot be created. */ public static @NonNull List createSharedFiles(boolean shouldEncryptCredentials) { return createFiles( Environment.getWifiSharedDirectory(), Arrays.asList(STORE_FILE_SHARED_GENERAL, STORE_FILE_SHARED_SOFTAP), UserHandle.ALL, shouldEncryptCredentials); } /** * Create new instances of the user specific store files. * The user store file is inside the user's encrypted data directory. * * @param userId userId corresponding to the currently logged-in user. * @param shouldEncryptCredentials Whether to encrypt credentials or not. * @return List of new instances of the store files created or null if the directory cannot be * created. */ public static @Nullable List createUserFiles(int userId, boolean shouldEncryptCredentials) { UserHandle userHandle = UserHandle.of(userId); return createFiles( Environment.getWifiUserDirectory(userId), Arrays.asList(STORE_FILE_USER_GENERAL, STORE_FILE_USER_NETWORK_SUGGESTIONS), userHandle, shouldEncryptCredentials); } /** * Enable verbose logging. */ public void enableVerboseLogging(boolean verbose) { mVerboseLoggingEnabled = verbose; } /** * Retrieve the list of {@link StoreData} instances registered for the provided * {@link StoreFile}. */ private List retrieveStoreDataListForStoreFile(@NonNull StoreFile storeFile) { return mStoreDataList .stream() .filter(s -> s.getStoreFileId() == storeFile.getFileId()) .collect(Collectors.toList()); } /** * Check if any of the provided list of {@link StoreData} instances registered * for the provided {@link StoreFile }have indicated that they have new data to serialize. */ private boolean hasNewDataToSerialize(@NonNull StoreFile storeFile) { List storeDataList = retrieveStoreDataListForStoreFile(storeFile); return storeDataList.stream().anyMatch(s -> s.hasNewDataToSerialize()); } /** * API to write the data provided by registered store data to config stores. * The method writes the user specific configurations to user specific config store and the * shared configurations to shared config store. * * @param forceSync boolean to force write the config stores now. if false, the writes are * buffered and written after the configured interval. */ public void write(boolean forceSync) throws XmlPullParserException, IOException { boolean hasAnyNewData = false; // Serialize the provided data and send it to the respective stores. The actual write will // be performed later depending on the |forceSync| flag . for (StoreFile sharedStoreFile : mSharedStores) { if (hasNewDataToSerialize(sharedStoreFile)) { byte[] sharedDataBytes = serializeData(sharedStoreFile); sharedStoreFile.storeRawDataToWrite(sharedDataBytes); hasAnyNewData = true; } } if (mUserStores != null) { for (StoreFile userStoreFile : mUserStores) { if (hasNewDataToSerialize(userStoreFile)) { byte[] userDataBytes = serializeData(userStoreFile); userStoreFile.storeRawDataToWrite(userDataBytes); hasAnyNewData = true; } } } if (hasAnyNewData) { // Every write provides a new snapshot to be persisted, so |forceSync| flag overrides // any pending buffer writes. if (forceSync) { writeBufferedData(); } else { startBufferedWriteAlarm(); } } else if (forceSync && mBufferedWritePending) { // no new data to write, but there is a pending buffered write. So, |forceSync| should // flush that out. writeBufferedData(); } } /** * Serialize all the data from all the {@link StoreData} clients registered for the provided * {@link StoreFile}. * * This method also computes the integrity of the data being written and serializes the computed * {@link EncryptedData} to the output. * * @param storeFile StoreFile that we want to write to. * @return byte[] of serialized bytes * @throws XmlPullParserException * @throws IOException */ private byte[] serializeData(@NonNull StoreFile storeFile) throws XmlPullParserException, IOException { List storeDataList = retrieveStoreDataListForStoreFile(storeFile); final XmlSerializer out = new FastXmlSerializer(); final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); out.setOutput(outputStream, StandardCharsets.UTF_8.name()); // First XML header. XmlUtil.writeDocumentStart(out, XML_TAG_DOCUMENT_HEADER); // Next version. XmlUtil.writeNextValue(out, XML_TAG_VERSION, CURRENT_CONFIG_STORE_DATA_VERSION); for (StoreData storeData : storeDataList) { String tag = storeData.getName(); XmlUtil.writeNextSectionStart(out, tag); storeData.serializeData(out, storeFile.getEncryptionUtil()); XmlUtil.writeNextSectionEnd(out, tag); } XmlUtil.writeDocumentEnd(out, XML_TAG_DOCUMENT_HEADER); return outputStream.toByteArray(); } /** * Helper method to start a buffered write alarm if one doesn't already exist. */ private void startBufferedWriteAlarm() { if (!mBufferedWritePending) { mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, mClock.getElapsedSinceBootMillis() + BUFFERED_WRITE_ALARM_INTERVAL_MS, BUFFERED_WRITE_ALARM_TAG, mBufferedWriteListener, mEventHandler); mBufferedWritePending = true; } } /** * Helper method to stop a buffered write alarm if one exists. */ private void stopBufferedWriteAlarm() { if (mBufferedWritePending) { mAlarmManager.cancel(mBufferedWriteListener); mBufferedWritePending = false; } } /** * Helper method to actually perform the writes to the file. This flushes out any write data * being buffered in the respective stores and cancels any pending buffer write alarms. */ private void writeBufferedData() throws IOException { stopBufferedWriteAlarm(); long writeStartTime = mClock.getElapsedSinceBootMillis(); for (StoreFile sharedStoreFile : mSharedStores) { sharedStoreFile.writeBufferedRawData(); } if (mUserStores != null) { for (StoreFile userStoreFile : mUserStores) { userStoreFile.writeBufferedRawData(); } } long writeTime = mClock.getElapsedSinceBootMillis() - writeStartTime; try { mWifiMetrics.noteWifiConfigStoreWriteDuration(toIntExact(writeTime)); } catch (ArithmeticException e) { // Silently ignore on any overflow errors. } Log.d(TAG, "Writing to stores completed in " + writeTime + " ms."); } /** * Note: This is a copy of {@link AtomicFile#readFully()} modified to use the passed in * {@link InputStream} which was returned using {@link AtomicFile#openRead()}. */ private static byte[] readAtomicFileFully(InputStream stream) throws IOException { try { int pos = 0; int avail = stream.available(); byte[] data = new byte[avail]; while (true) { int amt = stream.read(data, pos, data.length - pos); if (amt <= 0) { return data; } pos += amt; avail = stream.available(); if (avail > data.length - pos) { byte[] newData = new byte[pos + avail]; System.arraycopy(data, 0, newData, 0, pos); data = newData; } } } finally { stream.close(); } } /** * Conversion for file id's to use in WifiMigration API surface. */ private static Integer getMigrationStoreFileId(@StoreFileId int fileId) { switch (fileId) { case STORE_FILE_SHARED_GENERAL: return WifiMigration.STORE_FILE_SHARED_GENERAL; case STORE_FILE_SHARED_SOFTAP: return WifiMigration.STORE_FILE_SHARED_SOFTAP; case STORE_FILE_USER_GENERAL: return WifiMigration.STORE_FILE_USER_GENERAL; case STORE_FILE_USER_NETWORK_SUGGESTIONS: return WifiMigration.STORE_FILE_USER_NETWORK_SUGGESTIONS; default: return null; } } private static byte[] readDataFromMigrationSharedStoreFile(@StoreFileId int fileId) throws IOException { Integer migrationStoreFileId = getMigrationStoreFileId(fileId); if (migrationStoreFileId == null) return null; InputStream migrationIs = WifiMigration.convertAndRetrieveSharedConfigStoreFile(migrationStoreFileId); if (migrationIs == null) return null; return readAtomicFileFully(migrationIs); } private static byte[] readDataFromMigrationUserStoreFile(@StoreFileId int fileId, UserHandle userHandle) throws IOException { Integer migrationStoreFileId = getMigrationStoreFileId(fileId); if (migrationStoreFileId == null) return null; InputStream migrationIs = WifiMigration.convertAndRetrieveUserConfigStoreFile( migrationStoreFileId, userHandle); if (migrationIs == null) return null; return readAtomicFileFully(migrationIs); } /** * Helper method to read from the shared store files. * @throws XmlPullParserException * @throws IOException */ private void readFromSharedStoreFiles() throws XmlPullParserException, IOException { for (StoreFile sharedStoreFile : mSharedStores) { byte[] sharedDataBytes = readDataFromMigrationSharedStoreFile(sharedStoreFile.getFileId()); if (sharedDataBytes == null) { // nothing to migrate, do normal read. sharedDataBytes = sharedStoreFile.readRawData(); } else { Log.i(TAG, "Read data out of shared migration store file: " + sharedStoreFile.getName()); // Save the migrated file contents to the regular store file and delete the // migrated stored file. sharedStoreFile.storeRawDataToWrite(sharedDataBytes); sharedStoreFile.writeBufferedRawData(); // Note: If the migrated store file is at the same location as the store file, // then the OEM implementation should ignore this remove. WifiMigration.removeSharedConfigStoreFile( getMigrationStoreFileId(sharedStoreFile.getFileId())); } deserializeData(sharedDataBytes, sharedStoreFile); } } /** * Helper method to read from the user store files. * @throws XmlPullParserException * @throws IOException */ private void readFromUserStoreFiles() throws XmlPullParserException, IOException { for (StoreFile userStoreFile : mUserStores) { byte[] userDataBytes = readDataFromMigrationUserStoreFile( userStoreFile.getFileId(), userStoreFile.mUserHandle); if (userDataBytes == null) { // nothing to migrate, do normal read. userDataBytes = userStoreFile.readRawData(); } else { Log.i(TAG, "Read data out of user migration store file: " + userStoreFile.getName()); // Save the migrated file contents to the regular store file and delete the // migrated stored file. userStoreFile.storeRawDataToWrite(userDataBytes); userStoreFile.writeBufferedRawData(); // Note: If the migrated store file is at the same location as the store file, // then the OEM implementation should ignore this remove. WifiMigration.removeUserConfigStoreFile( getMigrationStoreFileId(userStoreFile.getFileId()), userStoreFile.mUserHandle); } deserializeData(userDataBytes, userStoreFile); } } /** * API to read the store data from the config stores. * The method reads the user specific configurations from user specific config store and the * shared configurations from the shared config store. */ public void read() throws XmlPullParserException, IOException { // Reset both share and user store data. for (StoreFile sharedStoreFile : mSharedStores) { resetStoreData(sharedStoreFile); } if (mUserStores != null) { for (StoreFile userStoreFile : mUserStores) { resetStoreData(userStoreFile); } } long readStartTime = mClock.getElapsedSinceBootMillis(); readFromSharedStoreFiles(); if (mUserStores != null) { readFromUserStoreFiles(); } long readTime = mClock.getElapsedSinceBootMillis() - readStartTime; try { mWifiMetrics.noteWifiConfigStoreReadDuration(toIntExact(readTime)); } catch (ArithmeticException e) { // Silently ignore on any overflow errors. } Log.d(TAG, "Reading from all stores completed in " + readTime + " ms."); } /** * Handles a user switch. This method changes the user specific store files and reads from the * new user's store files. * * @param userStores List of {@link StoreFile} created using {@link #createUserFiles(int)}. */ public void switchUserStoresAndRead(@NonNull List userStores) throws XmlPullParserException, IOException { Preconditions.checkNotNull(userStores); // Reset user store data. if (mUserStores != null) { for (StoreFile userStoreFile : mUserStores) { resetStoreData(userStoreFile); } } // Stop any pending buffered writes, if any. stopBufferedWriteAlarm(); mUserStores = userStores; // Now read from the user store files. long readStartTime = mClock.getElapsedSinceBootMillis(); readFromUserStoreFiles(); long readTime = mClock.getElapsedSinceBootMillis() - readStartTime; mWifiMetrics.noteWifiConfigStoreReadDuration(toIntExact(readTime)); Log.d(TAG, "Reading from user stores completed in " + readTime + " ms."); } /** * Reset data for all {@link StoreData} instances registered for this {@link StoreFile}. */ private void resetStoreData(@NonNull StoreFile storeFile) { for (StoreData storeData: retrieveStoreDataListForStoreFile(storeFile)) { storeData.resetData(); } } // Inform all the provided store data clients that there is nothing in the store for them. private void indicateNoDataForStoreDatas(Collection storeDataSet, @Version int version, @NonNull WifiConfigStoreEncryptionUtil encryptionUtil) throws XmlPullParserException, IOException { for (StoreData storeData : storeDataSet) { storeData.deserializeData(null, 0, version, encryptionUtil); } } /** * Deserialize data from a {@link StoreFile} for all {@link StoreData} instances registered. * * This method also computes the integrity of the incoming |dataBytes| and compare with * {@link EncryptedData} parsed from |dataBytes|. If the integrity check fails, the data * is discarded. * * @param dataBytes The data to parse * @param storeFile StoreFile that we read from. Will be used to retrieve the list of clients * who have data to deserialize from this file. * * @throws XmlPullParserException * @throws IOException */ private void deserializeData(@NonNull byte[] dataBytes, @NonNull StoreFile storeFile) throws XmlPullParserException, IOException { List storeDataList = retrieveStoreDataListForStoreFile(storeFile); if (dataBytes == null) { indicateNoDataForStoreDatas(storeDataList, -1 /* unknown */, storeFile.getEncryptionUtil()); return; } final XmlPullParser in = Xml.newPullParser(); final ByteArrayInputStream inputStream = new ByteArrayInputStream(dataBytes); in.setInput(inputStream, StandardCharsets.UTF_8.name()); // Start parsing the XML stream. int rootTagDepth = in.getDepth() + 1; XmlUtil.gotoDocumentStart(in, XML_TAG_DOCUMENT_HEADER); @Version int version = parseVersionFromXml(in); // Version 2 contains the now unused integrity data, parse & then discard the information. if (version == INTEGRITY_CONFIG_STORE_DATA_VERSION) { parseAndDiscardIntegrityDataFromXml(in, rootTagDepth); } String[] headerName = new String[1]; Set storeDatasInvoked = new HashSet<>(); while (XmlUtil.gotoNextSectionOrEnd(in, headerName, rootTagDepth)) { // There can only be 1 store data matching the tag, O indicates a previous StoreData // module that no longer exists (ignore this XML section). StoreData storeData = storeDataList.stream() .filter(s -> s.getSectionsToParse().contains(headerName[0])) .findAny() .orElse(null); if (storeData == null) { Log.e(TAG, "Unknown store data: " + headerName[0] + ". List of store data: " + storeDataList); continue; } storeData.deserializeDataForSection(in, rootTagDepth + 1, version, storeFile.getEncryptionUtil(), headerName[0]); storeDatasInvoked.add(storeData); } // Inform all the other registered store data clients that there is nothing in the store // for them. Set storeDatasNotInvoked = new HashSet<>(storeDataList); storeDatasNotInvoked.removeAll(storeDatasInvoked); indicateNoDataForStoreDatas(storeDatasNotInvoked, version, storeFile.getEncryptionUtil()); } /** * Parse the version from the XML stream. * This is used for both the shared and user config store data. * * @param in XmlPullParser instance pointing to the XML stream. * @return version number retrieved from the Xml stream. */ private static @Version int parseVersionFromXml(XmlPullParser in) throws XmlPullParserException, IOException { int version = (int) XmlUtil.readNextValueWithName(in, XML_TAG_VERSION); if (version < INITIAL_CONFIG_STORE_DATA_VERSION || version > CURRENT_CONFIG_STORE_DATA_VERSION) { throw new XmlPullParserException("Invalid version of data: " + version); } return version; } /** * Parse the integrity data structure from the XML stream and discard it. * * @param in XmlPullParser instance pointing to the XML stream. * @param outerTagDepth Outer tag depth. */ private static void parseAndDiscardIntegrityDataFromXml(XmlPullParser in, int outerTagDepth) throws XmlPullParserException, IOException { XmlUtil.gotoNextSectionWithName(in, XML_TAG_HEADER_INTEGRITY, outerTagDepth); XmlUtil.EncryptedDataXmlUtil.parseFromXml(in, outerTagDepth + 1); } /** * Dump the local log buffer and other internal state of WifiConfigManager. */ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("Dump of WifiConfigStore"); pw.println("WifiConfigStore - Store File Begin ----"); Stream.of(mSharedStores, mUserStores) .filter(Objects::nonNull) .flatMap(List::stream) .forEach((storeFile) -> { pw.print("Name: " + storeFile.mFileName); pw.print(", File Id: " + storeFile.mFileId); pw.println(", Credentials encrypted: " + (storeFile.getEncryptionUtil() != null)); }); pw.println("WifiConfigStore - Store Data Begin ----"); for (StoreData storeData : mStoreDataList) { pw.print("StoreData =>"); pw.print(" "); pw.print("Name: " + storeData.getName()); pw.print(", "); pw.print("File Id: " + storeData.getStoreFileId()); pw.print(", "); pw.println("File Name: " + STORE_ID_TO_FILE_NAME.get(storeData.getStoreFileId())); } pw.println("WifiConfigStore - Store Data End ----"); } /** * Class to encapsulate all file writes. This is a wrapper over {@link AtomicFile} to write/read * raw data from the persistent file with integrity. This class provides helper methods to * read/write the entire file into a byte array. * This helps to separate out the processing, parsing, and integrity checking from the actual * file writing. */ public static class StoreFile { /** * File permissions to lock down the file. */ private static final int FILE_MODE = 0600; /** * The store file to be written to. */ private final AtomicFile mAtomicFile; /** * This is an intermediate buffer to store the data to be written. */ private byte[] mWriteData; /** * Store the file name for setting the file permissions/logging purposes. */ private final String mFileName; /** * {@link StoreFileId} Type of store file. */ private final @StoreFileId int mFileId; /** * User handle. Meaningful only for user specific store files. */ private final UserHandle mUserHandle; /** * Integrity checking for the store file. */ private final WifiConfigStoreEncryptionUtil mEncryptionUtil; public StoreFile(File file, @StoreFileId int fileId, @NonNull UserHandle userHandle, @Nullable WifiConfigStoreEncryptionUtil encryptionUtil) { mAtomicFile = new AtomicFile(file); mFileName = file.getAbsolutePath(); mFileId = fileId; mUserHandle = userHandle; mEncryptionUtil = encryptionUtil; } public String getName() { return mAtomicFile.getBaseFile().getName(); } public @StoreFileId int getFileId() { return mFileId; } /** * @return Returns the encryption util used for this store file. */ public @Nullable WifiConfigStoreEncryptionUtil getEncryptionUtil() { return mEncryptionUtil; } /** * Read the entire raw data from the store file and return in a byte array. * * @return raw data read from the file or null if the file is not found or the data has * been altered. * @throws IOException if an error occurs. The input stream is always closed by the method * even when an exception is encountered. */ public byte[] readRawData() throws IOException { byte[] bytes = null; try { bytes = mAtomicFile.readFully(); } catch (FileNotFoundException e) { return null; } return bytes; } /** * Store the provided byte array to be written when {@link #writeBufferedRawData()} method * is invoked. * This intermediate step is needed to help in buffering file writes. * * @param data raw data to be written to the file. */ public void storeRawDataToWrite(byte[] data) { mWriteData = data; } /** * Write the stored raw data to the store file. * After the write to file, the mWriteData member is reset. * @throws IOException if an error occurs. The output stream is always closed by the method * even when an exception is encountered. */ public void writeBufferedRawData() throws IOException { if (mWriteData == null) return; // No data to write for this file. // Write the data to the atomic file. FileOutputStream out = null; try { out = mAtomicFile.startWrite(); FileUtils.chmod(mFileName, FILE_MODE); out.write(mWriteData); mAtomicFile.finishWrite(out); } catch (IOException e) { if (out != null) { mAtomicFile.failWrite(out); } throw e; } // Reset the pending write data after write. mWriteData = null; } } /** * Interface to be implemented by a module that contained data in the config store file. * * The module will be responsible for serializing/deserializing their own data. * Whenever {@link WifiConfigStore#read()} is invoked, all registered StoreData instances will * be notified that a read was performed via {@link StoreData#deserializeData( * XmlPullParser, int)} regardless of whether there is any data for them or not in the * store file. * * Note: StoreData clients that need a config store read to kick-off operations should wait * for the {@link StoreData#deserializeData(XmlPullParser, int)} invocation. */ public interface StoreData { /** * Serialize a XML data block to the output stream. * * @param out The output stream to serialize the data to * @param encryptionUtil Utility to help encrypt any credential data. */ void serializeData(XmlSerializer out, @Nullable WifiConfigStoreEncryptionUtil encryptionUtil) throws XmlPullParserException, IOException; /** * Deserialize a XML data block from the input stream. * * @param in The input stream to read the data from. This could be null if there is * nothing in the store. * @param outerTagDepth The depth of the outer tag in the XML document * @param version Version of config store file. * @param encryptionUtil Utility to help decrypt any credential data. * * Note: This will be invoked every time a store file is read, even if there is nothing * in the store for them. */ void deserializeData(@Nullable XmlPullParser in, int outerTagDepth, @Version int version, @Nullable WifiConfigStoreEncryptionUtil encryptionUtil) throws XmlPullParserException, IOException; /** * By default we will call the default deserializeData function. If some module needs to * parse data with non-default structure(for migration purposes), then override this method. */ default void deserializeDataForSection(@Nullable XmlPullParser in, int outerTagDepth, @Version int version, @Nullable WifiConfigStoreEncryptionUtil encryptionUtil, @NonNull String sectionName) throws XmlPullParserException, IOException { deserializeData(in, outerTagDepth, version, encryptionUtil); } /** * Reset configuration data. */ void resetData(); /** * Check if there is any new data to persist from the last write. * * @return true if the module has new data to persist, false otherwise. */ boolean hasNewDataToSerialize(); /** * Return the name of this store data. The data will be enclosed under this tag in * the XML block. * * @return The name of the store data */ String getName(); /** * By default, we parse the section the module writes. If some module needs to parse other * sections (for migration purposes), then override this method. * @return a set of section headers */ default HashSet getSectionsToParse() { // return new HashSet() {{ add(getName()); }}; } /** * File Id where this data needs to be written to. * This should be one of {@link #STORE_FILE_SHARED_GENERAL}, * {@link #STORE_FILE_USER_GENERAL} or * {@link #STORE_FILE_USER_NETWORK_SUGGESTIONS}. * * Note: For most uses, the shared or user general store is sufficient. Creating and * managing store files are expensive. Only use specific store files if you have a large * amount of data which may not need to be persisted frequently (or at least not as * frequently as the general store). * @return Id of the file where this data needs to be persisted. */ @StoreFileId int getStoreFileId(); } }