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      */
WifiConfigStore(Context context, Looper looper, Clock clock, StoreFile sharedStore)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 
setUserStore(StoreFile userStore)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      */
registerStoreData(StoreData storeData)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      */
createFile(File storeBaseDir)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      */
createSharedFile()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      */
createUserFile(int userId)219     public static StoreFile createUserFile(int userId) {
220         return createFile(Environment.getDataMiscCeDirectory(userId));
221     }
222 
223     /**
224      * Enable verbose logging.
225      */
enableVerboseLogging(boolean verbose)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      */
areStoresPresent()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      */
write(boolean forceSync)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      */
serializeData(boolean shareData)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      */
startBufferedWriteAlarm()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      */
stopBufferedWriteAlarm()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      */
writeBufferedData()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      */
read()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      */
switchUserStoreAndRead(StoreFile userStore)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      */
resetStoreData(boolean shareData)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.
indicateNoDataForStoreDatas(Collection<StoreData> storeDataSet, boolean shareData)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      */
deserializeData(byte[] dataBytes, boolean shareData)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      */
parseDocumentStartAndVersionFromXml(XmlPullParser in)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 
StoreFile(File file)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          */
exists()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          */
readRawData()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          */
storeRawDataToWrite(byte[] data)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          */
writeBufferedRawData()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          */
serializeData(XmlSerializer out, boolean shared)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          */
deserializeData(@ullable XmlPullParser in, int outerTagDepth, boolean shared)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          */
resetData(boolean shared)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          */
getName()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          */
supportShareData()621         boolean supportShareData();
622     }
623 }
624