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 static java.lang.Math.toIntExact;
20 
21 import android.annotation.IntDef;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.app.AlarmManager;
25 import android.content.Context;
26 import android.os.Environment;
27 import android.os.FileUtils;
28 import android.os.Handler;
29 import android.os.Looper;
30 import android.util.Log;
31 import android.util.SparseArray;
32 import android.util.Xml;
33 
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.internal.os.AtomicFile;
36 import com.android.internal.util.FastXmlSerializer;
37 import com.android.internal.util.Preconditions;
38 import com.android.server.wifi.util.EncryptedData;
39 import com.android.server.wifi.util.WifiConfigStoreEncryptionUtil;
40 import com.android.server.wifi.util.XmlUtil;
41 
42 import org.xmlpull.v1.XmlPullParser;
43 import org.xmlpull.v1.XmlPullParserException;
44 import org.xmlpull.v1.XmlSerializer;
45 
46 import java.io.ByteArrayInputStream;
47 import java.io.ByteArrayOutputStream;
48 import java.io.File;
49 import java.io.FileDescriptor;
50 import java.io.FileNotFoundException;
51 import java.io.FileOutputStream;
52 import java.io.IOException;
53 import java.io.PrintWriter;
54 import java.lang.annotation.Retention;
55 import java.lang.annotation.RetentionPolicy;
56 import java.nio.charset.StandardCharsets;
57 import java.util.ArrayList;
58 import java.util.Arrays;
59 import java.util.Collection;
60 import java.util.HashSet;
61 import java.util.List;
62 import java.util.Set;
63 import java.util.stream.Collectors;
64 import java.util.stream.Stream;
65 
66 /**
67  * This class provides a mechanism to save data to persistent store files {@link StoreFile}.
68  * Modules can register a {@link StoreData} instance indicating the {@StoreFile} into which they
69  * want to save their data to.
70  *
71  * NOTE:
72  * <li>Modules can register their {@StoreData} using
73  * {@link WifiConfigStore#registerStoreData(StoreData)} directly, but should
74  * use {@link WifiConfigManager#saveToStore(boolean)} for any writes.</li>
75  * <li>{@link WifiConfigManager} controls {@link WifiConfigStore} and initiates read at bootup and
76  * store file changes on user switch.</li>
77  * <li>Not thread safe!</li>
78  */
79 public class WifiConfigStore {
80     /**
81      * Config store file for general shared store file.
82      */
83     public static final int STORE_FILE_SHARED_GENERAL = 0;
84     /**
85      * Config store file for general user store file.
86      */
87     public static final int STORE_FILE_USER_GENERAL = 1;
88     /**
89      * Config store file for network suggestions user store file.
90      */
91     public static final int STORE_FILE_USER_NETWORK_SUGGESTIONS = 2;
92 
93     @IntDef(prefix = { "STORE_FILE_" }, value = {
94             STORE_FILE_SHARED_GENERAL,
95             STORE_FILE_USER_GENERAL,
96             STORE_FILE_USER_NETWORK_SUGGESTIONS
97     })
98     @Retention(RetentionPolicy.SOURCE)
99     public @interface StoreFileId { }
100 
101     private static final String XML_TAG_DOCUMENT_HEADER = "WifiConfigStoreData";
102     private static final String XML_TAG_VERSION = "Version";
103     private static final String XML_TAG_HEADER_INTEGRITY = "Integrity";
104     /**
105      * Current config store data version. This will be incremented for any additions.
106      */
107     private static final int CURRENT_CONFIG_STORE_DATA_VERSION = 3;
108     /** This list of older versions will be used to restore data from older config store. */
109     /**
110      * First version of the config store data format.
111      */
112     public static final int INITIAL_CONFIG_STORE_DATA_VERSION = 1;
113     /**
114      * Second version of the config store data format, introduced:
115      *  - Integrity info.
116      */
117     public static final int INTEGRITY_CONFIG_STORE_DATA_VERSION = 2;
118     /**
119      * Third version of the config store data format,
120      * introduced:
121      *  - Encryption of credentials
122      * removed:
123      *  - Integrity info.
124      */
125     public static final int ENCRYPT_CREDENTIALS_CONFIG_STORE_DATA_VERSION = 3;
126 
127     @IntDef(suffix = { "_VERSION" }, value = {
128             INITIAL_CONFIG_STORE_DATA_VERSION,
129             INTEGRITY_CONFIG_STORE_DATA_VERSION,
130             ENCRYPT_CREDENTIALS_CONFIG_STORE_DATA_VERSION
131     })
132     @Retention(RetentionPolicy.SOURCE)
133     public @interface Version { }
134 
135     /**
136      * Alarm tag to use for starting alarms for buffering file writes.
137      */
138     @VisibleForTesting
139     public static final String BUFFERED_WRITE_ALARM_TAG = "WriteBufferAlarm";
140     /**
141      * Log tag.
142      */
143     private static final String TAG = "WifiConfigStore";
144     /**
145      * Directory to store the config store files in.
146      */
147     private static final String STORE_DIRECTORY_NAME = "wifi";
148     /**
149      * Time interval for buffering file writes for non-forced writes
150      */
151     private static final int BUFFERED_WRITE_ALARM_INTERVAL_MS = 10 * 1000;
152     /**
153      * Config store file name for general shared store file.
154      */
155     private static final String STORE_FILE_NAME_SHARED_GENERAL = "WifiConfigStore.xml";
156     /**
157      * Config store file name for general user store file.
158      */
159     private static final String STORE_FILE_NAME_USER_GENERAL = "WifiConfigStore.xml";
160     /**
161      * Config store file name for network suggestions user store file.
162      */
163     private static final String STORE_FILE_NAME_USER_NETWORK_SUGGESTIONS =
164             "WifiConfigStoreNetworkSuggestions.xml";
165     /**
166      * Mapping of Store file Id to Store file names.
167      */
168     private static final SparseArray<String> STORE_ID_TO_FILE_NAME =
169             new SparseArray<String>() {{
170                 put(STORE_FILE_SHARED_GENERAL, STORE_FILE_NAME_SHARED_GENERAL);
171                 put(STORE_FILE_USER_GENERAL, STORE_FILE_NAME_USER_GENERAL);
172                 put(STORE_FILE_USER_NETWORK_SUGGESTIONS, STORE_FILE_NAME_USER_NETWORK_SUGGESTIONS);
173             }};
174     /**
175      * Handler instance to post alarm timeouts to
176      */
177     private final Handler mEventHandler;
178     /**
179      * Alarm manager instance to start buffer timeout alarms.
180      */
181     private final AlarmManager mAlarmManager;
182     /**
183      * Clock instance to retrieve timestamps for alarms.
184      */
185     private final Clock mClock;
186     private final WifiMetrics mWifiMetrics;
187     /**
188      * Shared config store file instance. There is 1 shared store file:
189      * {@link #STORE_FILE_NAME_SHARED_GENERAL}.
190      */
191     private StoreFile mSharedStore;
192     /**
193      * User specific store file instances. There are 2 user store files:
194      * {@link #STORE_FILE_NAME_USER_GENERAL} & {@link #STORE_FILE_NAME_USER_NETWORK_SUGGESTIONS}.
195      */
196     private List<StoreFile> mUserStores;
197     /**
198      * Verbose logging flag.
199      */
200     private boolean mVerboseLoggingEnabled = false;
201     /**
202      * Flag to indicate if there is a buffered write pending.
203      */
204     private boolean mBufferedWritePending = false;
205     /**
206      * Alarm listener for flushing out any buffered writes.
207      */
208     private final AlarmManager.OnAlarmListener mBufferedWriteListener =
209             new AlarmManager.OnAlarmListener() {
210                 public void onAlarm() {
211                     try {
212                         writeBufferedData();
213                     } catch (IOException e) {
214                         Log.wtf(TAG, "Buffered write failed", e);
215                     }
216                 }
217             };
218 
219     /**
220      * List of data containers.
221      */
222     private final List<StoreData> mStoreDataList;
223 
224     /**
225      * Create a new instance of WifiConfigStore.
226      * Note: The store file instances have been made inputs to this class to ease unit-testing.
227      *
228      * @param context     context to use for retrieving the alarm manager.
229      * @param looper      looper instance to post alarm timeouts to.
230      * @param clock       clock instance to retrieve timestamps for alarms.
231      * @param wifiMetrics Metrics instance.
232      * @param sharedStore StoreFile instance pointing to the shared store file. This should
233      *                    be retrieved using {@link #createSharedFile()} method.
234      */
WifiConfigStore(Context context, Looper looper, Clock clock, WifiMetrics wifiMetrics, StoreFile sharedStore)235     public WifiConfigStore(Context context, Looper looper, Clock clock, WifiMetrics wifiMetrics,
236             StoreFile sharedStore) {
237 
238         mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
239         mEventHandler = new Handler(looper);
240         mClock = clock;
241         mWifiMetrics = wifiMetrics;
242         mStoreDataList = new ArrayList<>();
243 
244         // Initialize the store files.
245         mSharedStore = sharedStore;
246         // The user store is initialized to null, this will be set when the user unlocks and
247         // CE storage is accessible via |switchUserStoresAndRead|.
248         mUserStores = null;
249     }
250 
251     /**
252      * Set the user store files.
253      * (Useful for mocking in unit tests).
254      * @param userStores List of {@link StoreFile} created using {@link #createUserFiles(int)}.
255      */
setUserStores(@onNull List<StoreFile> userStores)256     public void setUserStores(@NonNull List<StoreFile> userStores) {
257         Preconditions.checkNotNull(userStores);
258         mUserStores = userStores;
259     }
260 
261     /**
262      * Register a {@link StoreData} to read/write data from/to a store. A {@link StoreData} is
263      * responsible for a block of data in the store file, and provides serialization/deserialization
264      * functions for those data.
265      *
266      * @param storeData The store data to be registered to the config store
267      * @return true if registered successfully, false if the store file name is not valid.
268      */
registerStoreData(@onNull StoreData storeData)269     public boolean registerStoreData(@NonNull StoreData storeData) {
270         if (storeData == null) {
271             Log.e(TAG, "Unable to register null store data");
272             return false;
273         }
274         int storeFileId = storeData.getStoreFileId();
275         if (STORE_ID_TO_FILE_NAME.get(storeFileId) == null) {
276             Log.e(TAG, "Invalid shared store file specified" + storeFileId);
277             return false;
278         }
279         mStoreDataList.add(storeData);
280         return true;
281     }
282 
283     /**
284      * Helper method to create a store file instance for either the shared store or user store.
285      * Note: The method creates the store directory if not already present. This may be needed for
286      * user store files.
287      *
288      * @param storeBaseDir Base directory under which the store file is to be stored. The store file
289      *                     will be at <storeBaseDir>/wifi/WifiConfigStore.xml.
290      * @param fileId Identifier for the file. See {@link StoreFileId}.
291      * @param shouldEncryptCredentials Whether to encrypt credentials or not.
292      * @return new instance of the store file or null if the directory cannot be created.
293      */
createFile(File storeBaseDir, @StoreFileId int fileId, boolean shouldEncryptCredentials)294     private static @Nullable StoreFile createFile(File storeBaseDir, @StoreFileId int fileId,
295             boolean shouldEncryptCredentials) {
296         File storeDir = new File(storeBaseDir, STORE_DIRECTORY_NAME);
297         if (!storeDir.exists()) {
298             if (!storeDir.mkdir()) {
299                 Log.w(TAG, "Could not create store directory " + storeDir);
300                 return null;
301             }
302         }
303         File file = new File(storeDir, STORE_ID_TO_FILE_NAME.get(fileId));
304         WifiConfigStoreEncryptionUtil encryptionUtil = null;
305         if (shouldEncryptCredentials) {
306             encryptionUtil = new WifiConfigStoreEncryptionUtil(file.getName());
307         }
308         return new StoreFile(file, fileId, encryptionUtil);
309     }
310 
311     /**
312      * Create a new instance of the shared store file.
313      *
314      * @param shouldEncryptCredentials Whether to encrypt credentials or not.
315      * @return new instance of the store file or null if the directory cannot be created.
316      */
createSharedFile(boolean shouldEncryptCredentials)317     public static @Nullable StoreFile createSharedFile(boolean shouldEncryptCredentials) {
318         return createFile(Environment.getDataMiscDirectory(), STORE_FILE_SHARED_GENERAL,
319                 shouldEncryptCredentials);
320     }
321 
322     /**
323      * Create new instances of the user specific store files.
324      * The user store file is inside the user's encrypted data directory.
325      *
326      * @param userId userId corresponding to the currently logged-in user.
327      * @param shouldEncryptCredentials Whether to encrypt credentials or not.
328      * @return List of new instances of the store files created or null if the directory cannot be
329      * created.
330      */
createUserFiles(int userId, boolean shouldEncryptCredentials)331     public static @Nullable List<StoreFile> createUserFiles(int userId,
332             boolean shouldEncryptCredentials) {
333         List<StoreFile> storeFiles = new ArrayList<>();
334         for (int fileId : Arrays.asList(
335                 STORE_FILE_USER_GENERAL, STORE_FILE_USER_NETWORK_SUGGESTIONS)) {
336             StoreFile storeFile =
337                     createFile(Environment.getDataMiscCeDirectory(userId), fileId,
338                             shouldEncryptCredentials);
339             if (storeFile == null) {
340                 return null;
341             }
342             storeFiles.add(storeFile);
343         }
344         return storeFiles;
345     }
346 
347     /**
348      * Enable verbose logging.
349      */
enableVerboseLogging(boolean verbose)350     public void enableVerboseLogging(boolean verbose) {
351         mVerboseLoggingEnabled = verbose;
352     }
353 
354     /**
355      * API to check if any of the store files are present on the device. This can be used
356      * to detect if the device needs to perform data migration from legacy stores.
357      *
358      * @return true if any of the store file is present, false otherwise.
359      */
areStoresPresent()360     public boolean areStoresPresent() {
361         // Checking for the shared store file existence is sufficient since this is guaranteed
362         // to be present on migrated devices.
363         return mSharedStore.exists();
364     }
365 
366     /**
367      * Retrieve the list of {@link StoreData} instances registered for the provided
368      * {@link StoreFile}.
369      */
retrieveStoreDataListForStoreFile(@onNull StoreFile storeFile)370     private List<StoreData> retrieveStoreDataListForStoreFile(@NonNull StoreFile storeFile) {
371         return mStoreDataList
372                 .stream()
373                 .filter(s -> s.getStoreFileId() == storeFile.mFileId)
374                 .collect(Collectors.toList());
375     }
376 
377     /**
378      * Check if any of the provided list of {@link StoreData} instances registered
379      * for the provided {@link StoreFile }have indicated that they have new data to serialize.
380      */
hasNewDataToSerialize(@onNull StoreFile storeFile)381     private boolean hasNewDataToSerialize(@NonNull StoreFile storeFile) {
382         List<StoreData> storeDataList = retrieveStoreDataListForStoreFile(storeFile);
383         return storeDataList.stream().anyMatch(s -> s.hasNewDataToSerialize());
384     }
385 
386     /**
387      * API to write the data provided by registered store data to config stores.
388      * The method writes the user specific configurations to user specific config store and the
389      * shared configurations to shared config store.
390      *
391      * @param forceSync boolean to force write the config stores now. if false, the writes are
392      *                  buffered and written after the configured interval.
393      */
write(boolean forceSync)394     public void write(boolean forceSync)
395             throws XmlPullParserException, IOException {
396         boolean hasAnyNewData = false;
397         // Serialize the provided data and send it to the respective stores. The actual write will
398         // be performed later depending on the |forceSync| flag .
399         if (hasNewDataToSerialize(mSharedStore)) {
400             byte[] sharedDataBytes = serializeData(mSharedStore);
401             mSharedStore.storeRawDataToWrite(sharedDataBytes);
402             hasAnyNewData = true;
403         }
404         if (mUserStores != null) {
405             for (StoreFile userStoreFile : mUserStores) {
406                 if (hasNewDataToSerialize(userStoreFile)) {
407                     byte[] userDataBytes = serializeData(userStoreFile);
408                     userStoreFile.storeRawDataToWrite(userDataBytes);
409                     hasAnyNewData = true;
410                 }
411             }
412         }
413 
414         if (hasAnyNewData) {
415             // Every write provides a new snapshot to be persisted, so |forceSync| flag overrides
416             // any pending buffer writes.
417             if (forceSync) {
418                 writeBufferedData();
419             } else {
420                 startBufferedWriteAlarm();
421             }
422         } else if (forceSync && mBufferedWritePending) {
423             // no new data to write, but there is a pending buffered write. So, |forceSync| should
424             // flush that out.
425             writeBufferedData();
426         }
427     }
428 
429     /**
430      * Serialize all the data from all the {@link StoreData} clients registered for the provided
431      * {@link StoreFile}.
432      *
433      * This method also computes the integrity of the data being written and serializes the computed
434      * {@link EncryptedData} to the output.
435      *
436      * @param storeFile StoreFile that we want to write to.
437      * @return byte[] of serialized bytes
438      * @throws XmlPullParserException
439      * @throws IOException
440      */
serializeData(@onNull StoreFile storeFile)441     private byte[] serializeData(@NonNull StoreFile storeFile)
442             throws XmlPullParserException, IOException {
443         List<StoreData> storeDataList = retrieveStoreDataListForStoreFile(storeFile);
444 
445         final XmlSerializer out = new FastXmlSerializer();
446         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
447         out.setOutput(outputStream, StandardCharsets.UTF_8.name());
448 
449         // First XML header.
450         XmlUtil.writeDocumentStart(out, XML_TAG_DOCUMENT_HEADER);
451         // Next version.
452         XmlUtil.writeNextValue(out, XML_TAG_VERSION, CURRENT_CONFIG_STORE_DATA_VERSION);
453         for (StoreData storeData : storeDataList) {
454             String tag = storeData.getName();
455             XmlUtil.writeNextSectionStart(out, tag);
456             storeData.serializeData(out, storeFile.getEncryptionUtil());
457             XmlUtil.writeNextSectionEnd(out, tag);
458         }
459         XmlUtil.writeDocumentEnd(out, XML_TAG_DOCUMENT_HEADER);
460         return outputStream.toByteArray();
461     }
462 
463     /**
464      * Helper method to start a buffered write alarm if one doesn't already exist.
465      */
startBufferedWriteAlarm()466     private void startBufferedWriteAlarm() {
467         if (!mBufferedWritePending) {
468             mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
469                     mClock.getElapsedSinceBootMillis() + BUFFERED_WRITE_ALARM_INTERVAL_MS,
470                     BUFFERED_WRITE_ALARM_TAG, mBufferedWriteListener, mEventHandler);
471             mBufferedWritePending = true;
472         }
473     }
474 
475     /**
476      * Helper method to stop a buffered write alarm if one exists.
477      */
stopBufferedWriteAlarm()478     private void stopBufferedWriteAlarm() {
479         if (mBufferedWritePending) {
480             mAlarmManager.cancel(mBufferedWriteListener);
481             mBufferedWritePending = false;
482         }
483     }
484 
485     /**
486      * Helper method to actually perform the writes to the file. This flushes out any write data
487      * being buffered in the respective stores and cancels any pending buffer write alarms.
488      */
writeBufferedData()489     private void writeBufferedData() throws IOException {
490         stopBufferedWriteAlarm();
491 
492         long writeStartTime = mClock.getElapsedSinceBootMillis();
493         mSharedStore.writeBufferedRawData();
494         if (mUserStores != null) {
495             for (StoreFile userStoreFile : mUserStores) {
496                 userStoreFile.writeBufferedRawData();
497             }
498         }
499         long writeTime = mClock.getElapsedSinceBootMillis() - writeStartTime;
500         try {
501             mWifiMetrics.noteWifiConfigStoreWriteDuration(toIntExact(writeTime));
502         } catch (ArithmeticException e) {
503             // Silently ignore on any overflow errors.
504         }
505         Log.d(TAG, "Writing to stores completed in " + writeTime + " ms.");
506     }
507 
508     /**
509      * API to read the store data from the config stores.
510      * The method reads the user specific configurations from user specific config store and the
511      * shared configurations from the shared config store.
512      */
read()513     public void read() throws XmlPullParserException, IOException {
514         // Reset both share and user store data.
515         resetStoreData(mSharedStore);
516         if (mUserStores != null) {
517             for (StoreFile userStoreFile : mUserStores) {
518                 resetStoreData(userStoreFile);
519             }
520         }
521 
522         long readStartTime = mClock.getElapsedSinceBootMillis();
523         byte[] sharedDataBytes = mSharedStore.readRawData();
524         deserializeData(sharedDataBytes, mSharedStore);
525         if (mUserStores != null) {
526             for (StoreFile userStoreFile : mUserStores) {
527                 byte[] userDataBytes = userStoreFile.readRawData();
528                 deserializeData(userDataBytes, userStoreFile);
529             }
530         }
531         long readTime = mClock.getElapsedSinceBootMillis() - readStartTime;
532         try {
533             mWifiMetrics.noteWifiConfigStoreReadDuration(toIntExact(readTime));
534         } catch (ArithmeticException e) {
535             // Silently ignore on any overflow errors.
536         }
537         Log.d(TAG, "Reading from all stores completed in " + readTime + " ms.");
538     }
539 
540     /**
541      * Handles a user switch. This method changes the user specific store files and reads from the
542      * new user's store files.
543      *
544      * @param userStores List of {@link StoreFile} created using {@link #createUserFiles(int)}.
545      */
switchUserStoresAndRead(@onNull List<StoreFile> userStores)546     public void switchUserStoresAndRead(@NonNull List<StoreFile> userStores)
547             throws XmlPullParserException, IOException {
548         Preconditions.checkNotNull(userStores);
549         // Reset user store data.
550         if (mUserStores != null) {
551             for (StoreFile userStoreFile : mUserStores) {
552                 resetStoreData(userStoreFile);
553             }
554         }
555 
556         // Stop any pending buffered writes, if any.
557         stopBufferedWriteAlarm();
558         mUserStores = userStores;
559 
560         // Now read from the user store file.
561         long readStartTime = mClock.getElapsedSinceBootMillis();
562         for (StoreFile userStoreFile : mUserStores) {
563             byte[] userDataBytes = userStoreFile.readRawData();
564             deserializeData(userDataBytes, userStoreFile);
565         }
566         long readTime = mClock.getElapsedSinceBootMillis() - readStartTime;
567         mWifiMetrics.noteWifiConfigStoreReadDuration(toIntExact(readTime));
568         Log.d(TAG, "Reading from user stores completed in " + readTime + " ms.");
569     }
570 
571     /**
572      * Reset data for all {@link StoreData} instances registered for this {@link StoreFile}.
573      */
resetStoreData(@onNull StoreFile storeFile)574     private void resetStoreData(@NonNull StoreFile storeFile) {
575         for (StoreData storeData: retrieveStoreDataListForStoreFile(storeFile)) {
576             storeData.resetData();
577         }
578     }
579 
580     // Inform all the provided store data clients that there is nothing in the store for them.
indicateNoDataForStoreDatas(Collection<StoreData> storeDataSet, @Version int version, @NonNull WifiConfigStoreEncryptionUtil encryptionUtil)581     private void indicateNoDataForStoreDatas(Collection<StoreData> storeDataSet,
582             @Version int version, @NonNull WifiConfigStoreEncryptionUtil encryptionUtil)
583             throws XmlPullParserException, IOException {
584         for (StoreData storeData : storeDataSet) {
585             storeData.deserializeData(null, 0, version, encryptionUtil);
586         }
587     }
588 
589     /**
590      * Deserialize data from a {@link StoreFile} for all {@link StoreData} instances registered.
591      *
592      * This method also computes the integrity of the incoming |dataBytes| and compare with
593      * {@link EncryptedData} parsed from |dataBytes|. If the integrity check fails, the data
594      * is discarded.
595      *
596      * @param dataBytes The data to parse
597      * @param storeFile StoreFile that we read from. Will be used to retrieve the list of clients
598      *                  who have data to deserialize from this file.
599      *
600      * @throws XmlPullParserException
601      * @throws IOException
602      */
deserializeData(@onNull byte[] dataBytes, @NonNull StoreFile storeFile)603     private void deserializeData(@NonNull byte[] dataBytes, @NonNull StoreFile storeFile)
604             throws XmlPullParserException, IOException {
605         List<StoreData> storeDataList = retrieveStoreDataListForStoreFile(storeFile);
606         if (dataBytes == null) {
607             indicateNoDataForStoreDatas(storeDataList, -1 /* unknown */,
608                     storeFile.getEncryptionUtil());
609             return;
610         }
611         final XmlPullParser in = Xml.newPullParser();
612         final ByteArrayInputStream inputStream = new ByteArrayInputStream(dataBytes);
613         in.setInput(inputStream, StandardCharsets.UTF_8.name());
614 
615         // Start parsing the XML stream.
616         int rootTagDepth = in.getDepth() + 1;
617         XmlUtil.gotoDocumentStart(in, XML_TAG_DOCUMENT_HEADER);
618 
619         @Version int version = parseVersionFromXml(in);
620         // Version 2 contains the now unused integrity data, parse & then discard the information.
621         if (version == INTEGRITY_CONFIG_STORE_DATA_VERSION) {
622             parseAndDiscardIntegrityDataFromXml(in, rootTagDepth);
623         }
624 
625         String[] headerName = new String[1];
626         Set<StoreData> storeDatasInvoked = new HashSet<>();
627         while (XmlUtil.gotoNextSectionOrEnd(in, headerName, rootTagDepth)) {
628             // There can only be 1 store data matching the tag (O indicates a fatal
629             // error).
630             StoreData storeData = storeDataList.stream()
631                     .filter(s -> s.getName().equals(headerName[0]))
632                     .findAny()
633                     .orElse(null);
634             if (storeData == null) {
635                 throw new XmlPullParserException("Unknown store data: " + headerName[0]
636                         + ". List of store data: " + storeDataList);
637             }
638             storeData.deserializeData(in, rootTagDepth + 1, version,
639                     storeFile.getEncryptionUtil());
640             storeDatasInvoked.add(storeData);
641         }
642         // Inform all the other registered store data clients that there is nothing in the store
643         // for them.
644         Set<StoreData> storeDatasNotInvoked = new HashSet<>(storeDataList);
645         storeDatasNotInvoked.removeAll(storeDatasInvoked);
646         indicateNoDataForStoreDatas(storeDatasNotInvoked, version, storeFile.getEncryptionUtil());
647     }
648 
649     /**
650      * Parse the version from the XML stream.
651      * This is used for both the shared and user config store data.
652      *
653      * @param in XmlPullParser instance pointing to the XML stream.
654      * @return version number retrieved from the Xml stream.
655      */
parseVersionFromXml(XmlPullParser in)656     private static @Version int parseVersionFromXml(XmlPullParser in)
657             throws XmlPullParserException, IOException {
658         int version = (int) XmlUtil.readNextValueWithName(in, XML_TAG_VERSION);
659         if (version < INITIAL_CONFIG_STORE_DATA_VERSION
660                 || version > CURRENT_CONFIG_STORE_DATA_VERSION) {
661             throw new XmlPullParserException("Invalid version of data: " + version);
662         }
663         return version;
664     }
665 
666     /**
667      * Parse the integrity data structure from the XML stream and discard it.
668      *
669      * @param in XmlPullParser instance pointing to the XML stream.
670      * @param outerTagDepth Outer tag depth.
671      */
parseAndDiscardIntegrityDataFromXml(XmlPullParser in, int outerTagDepth)672     private static void parseAndDiscardIntegrityDataFromXml(XmlPullParser in, int outerTagDepth)
673             throws XmlPullParserException, IOException {
674         XmlUtil.gotoNextSectionWithName(in, XML_TAG_HEADER_INTEGRITY, outerTagDepth);
675         XmlUtil.EncryptedDataXmlUtil.parseFromXml(in, outerTagDepth + 1);
676     }
677 
678     /**
679      * Dump the local log buffer and other internal state of WifiConfigManager.
680      */
dump(FileDescriptor fd, PrintWriter pw, String[] args)681     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
682         pw.println("Dump of WifiConfigStore");
683         pw.println("WifiConfigStore - Store File Begin ----");
684         Stream.of(Arrays.asList(mSharedStore), mUserStores)
685                 .flatMap(List::stream)
686                 .forEach((storeFile) -> {
687                     pw.print("Name: " + storeFile.mFileName);
688                     pw.println(", Credentials encrypted: " + storeFile.getEncryptionUtil() != null);
689                 });
690         pw.println("WifiConfigStore - Store Data Begin ----");
691         for (StoreData storeData : mStoreDataList) {
692             pw.print("StoreData =>");
693             pw.print(" ");
694             pw.print("Name: " + storeData.getName());
695             pw.print(", ");
696             pw.print("File Id: " + storeData.getStoreFileId());
697             pw.print(", ");
698             pw.println("File Name: " + STORE_ID_TO_FILE_NAME.get(storeData.getStoreFileId()));
699         }
700         pw.println("WifiConfigStore - Store Data End ----");
701     }
702 
703     /**
704      * Class to encapsulate all file writes. This is a wrapper over {@link AtomicFile} to write/read
705      * raw data from the persistent file with integrity. This class provides helper methods to
706      * read/write the entire file into a byte array.
707      * This helps to separate out the processing, parsing, and integrity checking from the actual
708      * file writing.
709      */
710     public static class StoreFile {
711         /**
712          * File permissions to lock down the file.
713          */
714         private static final int FILE_MODE = 0600;
715         /**
716          * The store file to be written to.
717          */
718         private final AtomicFile mAtomicFile;
719         /**
720          * This is an intermediate buffer to store the data to be written.
721          */
722         private byte[] mWriteData;
723         /**
724          * Store the file name for setting the file permissions/logging purposes.
725          */
726         private final String mFileName;
727         /**
728          * {@link StoreFileId} Type of store file.
729          */
730         private final @StoreFileId int mFileId;
731         /**
732          * Integrity checking for the store file.
733          */
734         private final WifiConfigStoreEncryptionUtil mEncryptionUtil;
735 
StoreFile(File file, @StoreFileId int fileId, @Nullable WifiConfigStoreEncryptionUtil encryptionUtil)736         public StoreFile(File file, @StoreFileId int fileId,
737                 @Nullable WifiConfigStoreEncryptionUtil encryptionUtil) {
738             mAtomicFile = new AtomicFile(file);
739             mFileName = file.getAbsolutePath();
740             mFileId = fileId;
741             mEncryptionUtil = encryptionUtil;
742         }
743 
744         /**
745          * Returns whether the store file already exists on disk or not.
746          *
747          * @return true if it exists, false otherwise.
748          */
exists()749         public boolean exists() {
750             return mAtomicFile.exists();
751         }
752 
753         /**
754          * @return Returns the encryption util used for this store file.
755          */
getEncryptionUtil()756         public @Nullable WifiConfigStoreEncryptionUtil getEncryptionUtil() {
757             return mEncryptionUtil;
758         }
759 
760         /**
761          * Read the entire raw data from the store file and return in a byte array.
762          *
763          * @return raw data read from the file or null if the file is not found or the data has
764          *  been altered.
765          * @throws IOException if an error occurs. The input stream is always closed by the method
766          * even when an exception is encountered.
767          */
readRawData()768         public byte[] readRawData() throws IOException {
769             byte[] bytes = null;
770             try {
771                 bytes = mAtomicFile.readFully();
772             } catch (FileNotFoundException e) {
773                 return null;
774             }
775             return bytes;
776         }
777 
778         /**
779          * Store the provided byte array to be written when {@link #writeBufferedRawData()} method
780          * is invoked.
781          * This intermediate step is needed to help in buffering file writes.
782          *
783          * @param data raw data to be written to the file.
784          */
storeRawDataToWrite(byte[] data)785         public void storeRawDataToWrite(byte[] data) {
786             mWriteData = data;
787         }
788 
789         /**
790          * Write the stored raw data to the store file.
791          * After the write to file, the mWriteData member is reset.
792          * @throws IOException if an error occurs. The output stream is always closed by the method
793          * even when an exception is encountered.
794          */
writeBufferedRawData()795         public void writeBufferedRawData() throws IOException {
796             if (mWriteData == null) return; // No data to write for this file.
797             // Write the data to the atomic file.
798             FileOutputStream out = null;
799             try {
800                 out = mAtomicFile.startWrite();
801                 FileUtils.setPermissions(mFileName, FILE_MODE, -1, -1);
802                 out.write(mWriteData);
803                 mAtomicFile.finishWrite(out);
804             } catch (IOException e) {
805                 if (out != null) {
806                     mAtomicFile.failWrite(out);
807                 }
808                 throw e;
809             }
810             // Reset the pending write data after write.
811             mWriteData = null;
812         }
813     }
814 
815     /**
816      * Interface to be implemented by a module that contained data in the config store file.
817      *
818      * The module will be responsible for serializing/deserializing their own data.
819      * Whenever {@link WifiConfigStore#read()} is invoked, all registered StoreData instances will
820      * be notified that a read was performed via {@link StoreData#deserializeData(
821      * XmlPullParser, int)} regardless of whether there is any data for them or not in the
822      * store file.
823      *
824      * Note: StoreData clients that need a config store read to kick-off operations should wait
825      * for the {@link StoreData#deserializeData(XmlPullParser, int)} invocation.
826      */
827     public interface StoreData {
828         /**
829          * Serialize a XML data block to the output stream.
830          *
831          * @param out The output stream to serialize the data to
832          * @param encryptionUtil Utility to help encrypt any credential data.
833          */
serializeData(XmlSerializer out, @Nullable WifiConfigStoreEncryptionUtil encryptionUtil)834         void serializeData(XmlSerializer out,
835                 @Nullable WifiConfigStoreEncryptionUtil encryptionUtil)
836                 throws XmlPullParserException, IOException;
837 
838         /**
839          * Deserialize a XML data block from the input stream.
840          *
841          * @param in The input stream to read the data from. This could be null if there is
842          *           nothing in the store.
843          * @param outerTagDepth The depth of the outer tag in the XML document
844          * @param version Version of config store file.
845          * @param encryptionUtil Utility to help decrypt any credential data.
846          *
847          * Note: This will be invoked every time a store file is read, even if there is nothing
848          *                      in the store for them.
849          */
deserializeData(@ullable XmlPullParser in, int outerTagDepth, @Version int version, @Nullable WifiConfigStoreEncryptionUtil encryptionUtil)850         void deserializeData(@Nullable XmlPullParser in, int outerTagDepth, @Version int version,
851                 @Nullable WifiConfigStoreEncryptionUtil encryptionUtil)
852                 throws XmlPullParserException, IOException;
853 
854         /**
855          * Reset configuration data.
856          */
resetData()857         void resetData();
858 
859         /**
860          * Check if there is any new data to persist from the last write.
861          *
862          * @return true if the module has new data to persist, false otherwise.
863          */
hasNewDataToSerialize()864         boolean hasNewDataToSerialize();
865 
866         /**
867          * Return the name of this store data.  The data will be enclosed under this tag in
868          * the XML block.
869          *
870          * @return The name of the store data
871          */
getName()872         String getName();
873 
874         /**
875          * File Id where this data needs to be written to.
876          * This should be one of {@link #STORE_FILE_SHARED_GENERAL},
877          * {@link #STORE_FILE_USER_GENERAL} or
878          * {@link #STORE_FILE_USER_NETWORK_SUGGESTIONS}.
879          *
880          * Note: For most uses, the shared or user general store is sufficient. Creating and
881          * managing store files are expensive. Only use specific store files if you have a large
882          * amount of data which may not need to be persisted frequently (or at least not as
883          * frequently as the general store).
884          * @return Id of the file where this data needs to be persisted.
885          */
getStoreFileId()886         @StoreFileId int getStoreFileId();
887     }
888 }
889