| /* |
| * 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: |
| * <li>Modules can register their {@StoreData} using |
| * {@link WifiConfigStore#registerStoreData(StoreData)} directly, but should |
| * use {@link WifiConfigManager#saveToStore(boolean)} for any writes.</li> |
| * <li>{@link WifiConfigManager} controls {@link WifiConfigStore} and initiates read at bootup and |
| * store file changes on user switch.</li> |
| * <li>Not thread safe!</li> |
| */ |
| 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<String> STORE_ID_TO_FILE_NAME = |
| new SparseArray<String>() {{ |
| 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<StoreFile> 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<StoreFile> 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<StoreData> 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<StoreFile> 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<StoreFile> 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 <storeDir>/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<StoreFile> createFiles(File storeDir, List<Integer> storeFileIds, |
| UserHandle userHandle, boolean shouldEncryptCredentials) { |
| List<StoreFile> 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<StoreFile> 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<StoreFile> 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<StoreData> 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<StoreData> 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<StoreData> 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<StoreFile> 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<StoreData> 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<StoreData> 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<StoreData> 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<StoreData> 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<String> getSectionsToParse() { |
| // |
| return new HashSet<String>() {{ 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(); |
| } |
| } |