blob: 659192be45f58a7f06da6e8c23169dee08160b32 [file] [log] [blame]
/*
* 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 android.app.AlarmManager;
import android.content.Context;
import android.os.Environment;
import android.os.FileUtils;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.util.Xml;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.AtomicFile;
import com.android.internal.util.FastXmlSerializer;
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.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* This class provides the API's to save/load/modify network configurations from a persistent
* store. Uses keystore for certificate/key management operations.
* NOTE: This class should only be used from WifiConfigManager and is not thread-safe!
*/
public class WifiConfigStore {
private static final String XML_TAG_DOCUMENT_HEADER = "WifiConfigStoreData";
private static final String XML_TAG_VERSION = "Version";
/**
* Current config store data version. This will be incremented for any additions.
*/
private static final int CURRENT_CONFIG_STORE_DATA_VERSION = 1;
/** This list of older versions will be used to restore data from older config store. */
/**
* First version of the config store data format.
*/
private static final int INITIAL_CONFIG_STORE_DATA_VERSION = 1;
/**
* 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";
/**
* Config store file name for both shared & user specific stores.
*/
private static final String STORE_FILE_NAME = "WifiConfigStore.xml";
/**
* Directory to store the config store files in.
*/
private static final String STORE_DIRECTORY_NAME = "wifi";
/**
* Time interval for buffering file writes for non-forced writes
*/
private static final int BUFFERED_WRITE_ALARM_INTERVAL_MS = 10 * 1000;
/**
* 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;
/**
* Shared config store file instance.
*/
private StoreFile mSharedStore;
/**
* User specific store file instance.
*/
private StoreFile mUserStore;
/**
* 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 container.
*/
private final Map<String, 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 looper looper instance to post alarm timeouts to.
* @param clock clock instance to retrieve timestamps for alarms.
* @param sharedStore StoreFile instance pointing to the shared store file. This should
* be retrieved using {@link #createSharedFile()} method.
*/
public WifiConfigStore(Context context, Looper looper, Clock clock,
StoreFile sharedStore) {
mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
mEventHandler = new Handler(looper);
mClock = clock;
mStoreDataList = new HashMap<>();
// Initialize the store files.
mSharedStore = sharedStore;
// The user store is initialized to null, this will be set when the user unlocks and
// CE storage is accessible via |switchUserStoreAndRead|.
mUserStore = null;
}
public void setUserStore(StoreFile userStore) {
mUserStore = userStore;
}
/**
* Register a {@link StoreData} to 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 succeeded
*/
public boolean registerStoreData(StoreData storeData) {
if (storeData == null) {
Log.e(TAG, "Unable to register null store data");
return false;
}
mStoreDataList.put(storeData.getName(), 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 storeBaseDir Base directory under which the store file is to be stored. The store file
* will be at <storeBaseDir>/wifi/WifiConfigStore.xml.
* @return new instance of the store file.
*/
private static StoreFile createFile(File storeBaseDir) {
File storeDir = new File(storeBaseDir, STORE_DIRECTORY_NAME);
if (!storeDir.exists()) {
if (!storeDir.mkdir()) {
Log.w(TAG, "Could not create store directory " + storeDir);
}
}
return new StoreFile(new File(storeDir, STORE_FILE_NAME));
}
/**
* Create a new instance of the shared store file.
*
* @return new instance of the store file or null if the directory cannot be created.
*/
public static StoreFile createSharedFile() {
return createFile(Environment.getDataMiscDirectory());
}
/**
* Create a new instance of the user specific store file.
* The user store file is inside the user's encrypted data directory.
*
* @param userId userId corresponding to the currently logged-in user.
* @return new instance of the store file or null if the directory cannot be created.
*/
public static StoreFile createUserFile(int userId) {
return createFile(Environment.getDataMiscCeDirectory(userId));
}
/**
* Enable verbose logging.
*/
public void enableVerboseLogging(boolean verbose) {
mVerboseLoggingEnabled = verbose;
}
/**
* API to check if any of the store files are present on the device. This can be used
* to detect if the device needs to perform data migration from legacy stores.
*
* @return true if any of the store file is present, false otherwise.
*/
public boolean areStoresPresent() {
return (mSharedStore.exists() || (mUserStore != null && mUserStore.exists()));
}
/**
* 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 {
// Serialize the provided data and send it to the respective stores. The actual write will
// be performed later depending on the |forceSync| flag .
byte[] sharedDataBytes = serializeData(true);
mSharedStore.storeRawDataToWrite(sharedDataBytes);
if (mUserStore != null) {
byte[] userDataBytes = serializeData(false);
mUserStore.storeRawDataToWrite(userDataBytes);
}
// Every write provides a new snapshot to be persisted, so |forceSync| flag overrides any
// pending buffer writes.
if (forceSync) {
writeBufferedData();
} else {
startBufferedWriteAlarm();
}
}
/**
* Serialize share data or user data from all store data.
*
* @param shareData Flag indicating share data
* @return byte[] of serialized bytes
* @throws XmlPullParserException
* @throws IOException
*/
private byte[] serializeData(boolean shareData) throws XmlPullParserException, IOException {
final XmlSerializer out = new FastXmlSerializer();
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
out.setOutput(outputStream, StandardCharsets.UTF_8.name());
XmlUtil.writeDocumentStart(out, XML_TAG_DOCUMENT_HEADER);
XmlUtil.writeNextValue(out, XML_TAG_VERSION, CURRENT_CONFIG_STORE_DATA_VERSION);
for (Map.Entry<String, StoreData> entry : mStoreDataList.entrySet()) {
String tag = entry.getKey();
StoreData storeData = entry.getValue();
// Ignore this store data if this is for share file and the store data doesn't support
// share store.
if (shareData && !storeData.supportShareData()) {
continue;
}
XmlUtil.writeNextSectionStart(out, tag);
storeData.serializeData(out, shareData);
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();
mSharedStore.writeBufferedRawData();
if (mUserStore != null) {
mUserStore.writeBufferedRawData();
}
long writeTime = mClock.getElapsedSinceBootMillis() - writeStartTime;
Log.d(TAG, "Writing to stores completed in " + writeTime + " ms.");
}
/**
* 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.
resetStoreData(true);
resetStoreData(false);
long readStartTime = mClock.getElapsedSinceBootMillis();
byte[] sharedDataBytes = mSharedStore.readRawData();
byte[] userDataBytes = null;
if (mUserStore != null) {
userDataBytes = mUserStore.readRawData();
}
long readTime = mClock.getElapsedSinceBootMillis() - readStartTime;
Log.d(TAG, "Reading from stores completed in " + readTime + " ms.");
deserializeData(sharedDataBytes, true);
deserializeData(userDataBytes, false);
}
/**
* Handles a user switch. This method changes the user specific store file and reads from the
* new user's store file.
*
* @param userStore StoreFile instance pointing to the user specific store file. This should
* be retrieved using {@link #createUserFile(int)} method.
*/
public void switchUserStoreAndRead(StoreFile userStore)
throws XmlPullParserException, IOException {
// Reset user store data.
resetStoreData(false);
// Stop any pending buffered writes, if any.
stopBufferedWriteAlarm();
mUserStore = userStore;
// Now read from the user store file.
long readStartTime = mClock.getElapsedSinceBootMillis();
byte[] userDataBytes = mUserStore.readRawData();
long readTime = mClock.getElapsedSinceBootMillis() - readStartTime;
Log.d(TAG, "Reading from user store completed in " + readTime + " ms.");
deserializeData(userDataBytes, false);
}
/**
* Reset share data or user data in all store data.
*
* @param shareData Flag indicating share data
*/
private void resetStoreData(boolean shareData) {
for (Map.Entry<String, StoreData> entry : mStoreDataList.entrySet()) {
entry.getValue().resetData(shareData);
}
}
/**
* Deserialize share data or user data into store data.
*
* @param dataBytes The data to parse
* @param shareData The flag indicating share data
* @throws XmlPullParserException
* @throws IOException
*/
private void deserializeData(byte[] dataBytes, boolean shareData)
throws XmlPullParserException, IOException {
if (dataBytes == null) {
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;
parseDocumentStartAndVersionFromXml(in);
String[] headerName = new String[1];
while (XmlUtil.gotoNextSectionOrEnd(in, headerName, rootTagDepth)) {
StoreData storeData = mStoreDataList.get(headerName[0]);
if (storeData == null) {
throw new XmlPullParserException("Unknown store data: " + headerName[0]);
}
storeData.deserializeData(in, rootTagDepth + 1, shareData);
}
}
/**
* Parse the document start and 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 int parseDocumentStartAndVersionFromXml(XmlPullParser in)
throws XmlPullParserException, IOException {
XmlUtil.gotoDocumentStart(in, XML_TAG_DOCUMENT_HEADER);
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;
}
/**
* Class to encapsulate all file writes. This is a wrapper over {@link AtomicFile} to write/read
* raw data from the persistent file. This class provides helper methods to read/write the
* entire file into a byte array.
* This helps to separate out the processing/parsing 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 String mFileName;
public StoreFile(File file) {
mAtomicFile = new AtomicFile(file);
mFileName = mAtomicFile.getBaseFile().getAbsolutePath();
}
/**
* Returns whether the store file already exists on disk or not.
*
* @return true if it exists, false otherwise.
*/
public boolean exists() {
return mAtomicFile.exists();
}
/**
* 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.
* @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 {
try {
return mAtomicFile.readFully();
} catch (FileNotFoundException e) {
return null;
}
}
/**
* 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) {
Log.w(TAG, "No data stored for writing to file: " + mFileName);
return;
}
// Write the data to the atomic file.
FileOutputStream out = null;
try {
out = mAtomicFile.startWrite();
FileUtils.setPermissions(mFileName, FILE_MODE, -1, -1);
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.
*/
public interface StoreData {
/**
* Serialize a XML data block to the output stream. The |shared| flag indicates if the
* output stream is backed by a share store or an user store.
*
* @param out The output stream to serialize the data to
* @param shared Flag indicating if the output stream is backed by a share store or an
* user store
*/
void serializeData(XmlSerializer out, boolean shared)
throws XmlPullParserException, IOException;
/**
* Deserialize a XML data block from the input stream. The |shared| flag indicates if the
* input stream is backed by a share store or an user store. When |shared| is set to true,
* the shared configuration data will be overwritten by the parsed data. Otherwise,
* the user configuration will be overwritten by the parsed data.
*
* @param in The input stream to read the data from
* @param outerTagDepth The depth of the outer tag in the XML document
* @Param shared Flag indicating if the input stream is backed by a share store or an
* user store
*/
void deserializeData(XmlPullParser in, int outerTagDepth, boolean shared)
throws XmlPullParserException, IOException;
/**
* Reset configuration data. The |shared| flag indicates which configuration data to
* reset. When |shared| is set to true, the shared configuration data will be reset.
* Otherwise, the user configuration data will be reset.
*/
void resetData(boolean shared);
/**
* 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();
/**
* Flag indicating if shared configuration data is supported.
*
* @return true if shared configuration data is supported
*/
boolean supportShareData();
}
}