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
17package com.android.server.wifi;
18
19import android.annotation.Nullable;
20import android.app.AlarmManager;
21import android.content.Context;
22import android.os.Environment;
23import android.os.FileUtils;
24import android.os.Handler;
25import android.os.Looper;
26import android.util.Log;
27import android.util.Xml;
28
29import com.android.internal.annotations.VisibleForTesting;
30import com.android.internal.os.AtomicFile;
31import com.android.internal.util.FastXmlSerializer;
32import com.android.server.wifi.util.XmlUtil;
33
34import org.xmlpull.v1.XmlPullParser;
35import org.xmlpull.v1.XmlPullParserException;
36import org.xmlpull.v1.XmlSerializer;
37
38import java.io.ByteArrayInputStream;
39import java.io.ByteArrayOutputStream;
40import java.io.File;
41import java.io.FileNotFoundException;
42import java.io.FileOutputStream;
43import java.io.IOException;
44import java.nio.charset.StandardCharsets;
45import java.util.Collection;
46import java.util.HashMap;
47import java.util.HashSet;
48import java.util.Map;
49import 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 */
56public 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