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