• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
WifiConfigStore(Context context, Looper looper, Clock clock, StoreFile sharedStore)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 
setUserStore(StoreFile userStore)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      */
registerStoreData(StoreData storeData)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      */
createFile(File storeBaseDir)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      */
createSharedFile()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      */
createUserFile(int userId)215     public static StoreFile createUserFile(int userId) {
216         return createFile(Environment.getDataMiscCeDirectory(userId));
217     }
218 
219     /**
220      * Enable verbose logging.
221      */
enableVerboseLogging(boolean verbose)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      */
areStoresPresent()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      */
write(boolean forceSync)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      */
serializeData(boolean shareData)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      */
startBufferedWriteAlarm()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      */
stopBufferedWriteAlarm()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      */
writeBufferedData()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      */
read()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      */
switchUserStoreAndRead(StoreFile userStore)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      */
resetStoreData(boolean shareData)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      */
deserializeData(byte[] dataBytes, boolean shareData)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      */
parseDocumentStartAndVersionFromXml(XmlPullParser in)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 
StoreFile(File file)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          */
exists()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          */
readRawData()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          */
storeRawDataToWrite(byte[] data)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          */
writeBufferedRawData()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          */
serializeData(XmlSerializer out, boolean shared)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          */
deserializeData(XmlPullParser in, int outerTagDepth, boolean shared)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          */
resetData(boolean shared)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          */
getName()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          */
supportShareData()586         boolean supportShareData();
587     }
588 }
589