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