[MAC rand] Removing persistent storage

Will now calculate the randomized MAC address directly for new wifi
networks.
For existing networks that already have saved randomized MAC address, we
will continue to use the saved MAC.

Bug: 140065828
Test: unit tests before and after factory reset
Test: Verified that new MAC address entries are not populated into
WifiConfigStore.xml
Test: Manually verified calculated MAC addresses are consistent and
valid.
Test: Manually verified that the device secret is persisted over
different builds

Change-Id: Ie216fbaf33e2582ae1a0030c5046b0d4162255d9
Merged-In: I4b4e2cc6fe304d277661c4743e4fb86bb7500f16
(cherry picked from commit I4b4e2cc6fe304d277661c4743e4fb86bb7500f16)
(cherry picked from commit 77ea7bf52f2f08066ac74f49478301e81f579f31)
diff --git a/service/java/com/android/server/wifi/WifiConfigManager.java b/service/java/com/android/server/wifi/WifiConfigManager.java
index 683ace4..d1734d4 100644
--- a/service/java/com/android/server/wifi/WifiConfigManager.java
+++ b/service/java/com/android/server/wifi/WifiConfigManager.java
@@ -76,6 +76,8 @@
 import java.util.Map;
 import java.util.Set;
 
+import javax.crypto.Mac;
+
 /**
  * This class provides the APIs to manage configured Wi-Fi networks.
  * It deals with the following:
@@ -228,6 +230,9 @@
     private static final int WIFI_PNO_FREQUENCY_CULLING_ENABLED_DEFAULT = 1; // 0 = disabled
     private static final int WIFI_PNO_RECENCY_SORTING_ENABLED_DEFAULT = 1; // 0 = disabled:
 
+    private static final MacAddress DEFAULT_MAC_ADDRESS =
+            MacAddress.fromString(WifiInfo.DEFAULT_MAC_ADDRESS);
+
     /**
      * Expiration timeout for deleted ephemeral ssids. (1 day)
      */
@@ -272,6 +277,7 @@
     private final WifiPermissionsWrapper mWifiPermissionsWrapper;
     private final WifiInjector mWifiInjector;
     private boolean mConnectedMacRandomzationSupported;
+    private final Mac mMac;
 
     /**
      * Local log used for debugging any WifiConfigManager issues.
@@ -446,6 +452,11 @@
         } catch (PackageManager.NameNotFoundException e) {
             Log.e(TAG, "Unable to resolve SystemUI's UID.");
         }
+        mMac = WifiConfigurationUtil.obtainMacRandHashFunction(Process.WIFI_UID);
+        if (mMac == null) {
+            Log.wtf(TAG, "Failed to obtain secret for MAC randomization."
+                    + " All randomized MAC addresses are lost!");
+        }
     }
 
     /**
@@ -464,6 +475,59 @@
         return sb.toString();
     }
 
+    @VisibleForTesting
+    protected int getRandomizedMacAddressMappingSize() {
+        return mRandomizedMacAddressMapping.size();
+    }
+
+    /**
+     * The persistent randomized MAC address is locally generated for each SSID and does not
+     * change until factory reset of the device. In the initial Q release the per-SSID randomized
+     * MAC is saved on the device, but in an update the storing of randomized MAC is removed.
+     * Instead, the randomized MAC is calculated directly from the SSID and a on device secret.
+     * For backward compatibility, this method first checks the device storage for saved
+     * randomized MAC. If it is not found or the saved MAC is invalid then it will calculate the
+     * randomized MAC directly.
+     *
+     * In the future as devices launched on Q no longer get supported, this method should get
+     * simplified to return the calculated MAC address directly.
+     * @param config the WifiConfiguration to obtain MAC address for.
+     * @return persistent MAC address for this WifiConfiguration
+     */
+    private MacAddress getPersistentMacAddress(WifiConfiguration config) {
+        // mRandomizedMacAddressMapping had been the location to save randomized MAC addresses.
+        String persistentMacString = mRandomizedMacAddressMapping.get(
+                config.getSsidAndSecurityTypeString());
+        // Use the MAC address stored in the storage if it exists and is valid. Otherwise
+        // use the MAC address calculated from a hash function as the persistent MAC.
+        if (persistentMacString != null) {
+            try {
+                return MacAddress.fromString(persistentMacString);
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "Error creating randomized MAC address from stored value.");
+                mRandomizedMacAddressMapping.remove(config.getSsidAndSecurityTypeString());
+            }
+        }
+        return WifiConfigurationUtil.calculatePersistentMacForConfiguration(config, mMac);
+    }
+
+    /**
+     * Obtain the persistent MAC address by first reading from an internal database. If non exists
+     * then calculate the persistent MAC using HMAC-SHA256.
+     * Finally set the randomized MAC of the configuration to the randomized MAC obtained.
+     * @param config the WifiConfiguration to make the update
+     * @return the persistent MacAddress or null if the operation is unsuccessful
+     */
+    private MacAddress setRandomizedMacToPersistentMac(WifiConfiguration config) {
+        MacAddress persistentMac = getPersistentMacAddress(config);
+        if (persistentMac == null || persistentMac.equals(config.getRandomizedMacAddress())) {
+            return persistentMac;
+        }
+        WifiConfiguration internalConfig = getInternalConfiguredNetwork(config.networkId);
+        internalConfig.setRandomizedMacAddress(persistentMac);
+        return persistentMac;
+    }
+
     /**
      * Enable/disable verbose logging in WifiConfigManager & its helper classes.
      */
@@ -520,8 +584,7 @@
      * @param configuration WifiConfiguration to hide the MAC address
      */
     private void maskRandomizedMacAddressInWifiConfiguration(WifiConfiguration configuration) {
-        MacAddress defaultMac = MacAddress.fromString(WifiInfo.DEFAULT_MAC_ADDRESS);
-        configuration.setRandomizedMacAddress(defaultMac);
+        configuration.setRandomizedMacAddress(DEFAULT_MAC_ADDRESS);
     }
 
     /**
@@ -1050,34 +1113,11 @@
                 packageName != null ? packageName : mContext.getPackageManager().getNameForUid(uid);
         newInternalConfig.creationTime = newInternalConfig.updateTime =
                 createDebugTimeStampString(mClock.getWallClockMillis());
-        updateRandomizedMacAddress(newInternalConfig);
-
-        return newInternalConfig;
-    }
-
-    /**
-     * Sets the randomized address for the given configuration from stored map if it exist.
-     * Otherwise generates a new randomized address and save to the stored map.
-     * @param config
-     */
-    private void updateRandomizedMacAddress(WifiConfiguration config) {
-        // Update randomized MAC address according to stored map
-        final String key = config.getSsidAndSecurityTypeString();
-        // If the key is not found in the current store, then it means this network has never been
-        // seen before. So add it to store.
-        if (!mRandomizedMacAddressMapping.containsKey(key)) {
-            mRandomizedMacAddressMapping.put(key,
-                    config.getOrCreateRandomizedMacAddress().toString());
-        } else { // Otherwise read from the store and set the WifiConfiguration
-            try {
-                config.setRandomizedMacAddress(
-                        MacAddress.fromString(mRandomizedMacAddressMapping.get(key)));
-            } catch (IllegalArgumentException e) {
-                Log.e(TAG, "Error creating randomized MAC address from stored value.");
-                mRandomizedMacAddressMapping.put(key,
-                        config.getOrCreateRandomizedMacAddress().toString());
-            }
+        MacAddress randomizedMac = getPersistentMacAddress(newInternalConfig);
+        if (randomizedMac != null) {
+            newInternalConfig.setRandomizedMacAddress(randomizedMac);
         }
+        return newInternalConfig;
     }
 
     /**
@@ -3026,12 +3066,16 @@
     }
 
     /**
-     * Generate randomized MAC addresses for configured networks and persist mapping to storage.
+     * Assign randomized MAC addresses for configured networks.
+     * This is needed to generate persistent randomized MAC address for existing networks when
+     * a device updates to Q+ for the first time since we are not calling addOrUpdateNetwork when
+     * we load configuration at boot.
      */
     private void generateRandomizedMacAddresses() {
         for (WifiConfiguration config : getInternalConfiguredNetworks()) {
-            mRandomizedMacAddressMapping.put(config.getSsidAndSecurityTypeString(),
-                    config.getOrCreateRandomizedMacAddress().toString());
+            if (DEFAULT_MAC_ADDRESS.equals(config.getRandomizedMacAddress())) {
+                setRandomizedMacToPersistentMac(config);
+            }
         }
     }
 
diff --git a/service/java/com/android/server/wifi/WifiConfigurationUtil.java b/service/java/com/android/server/wifi/WifiConfigurationUtil.java
index 59d3eb3..b8992a0 100644
--- a/service/java/com/android/server/wifi/WifiConfigurationUtil.java
+++ b/service/java/com/android/server/wifi/WifiConfigurationUtil.java
@@ -28,6 +28,9 @@
 import android.net.wifi.WifiScanner;
 import android.os.PatternMatcher;
 import android.os.UserHandle;
+import android.security.keystore.AndroidKeyStoreProvider;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
@@ -36,7 +39,17 @@
 import com.android.server.wifi.util.NativeUtil;
 import com.android.server.wifi.util.TelephonyUtil;
 
+import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.ProviderException;
+import java.security.UnrecoverableKeyException;
 import java.security.cert.X509Certificate;
 import java.util.Arrays;
 import java.util.BitSet;
@@ -44,6 +57,10 @@
 import java.util.List;
 import java.util.Objects;
 
+import javax.crypto.KeyGenerator;
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+
 /**
  * WifiConfiguration utility for any {@link android.net.wifi.WifiConfiguration} related operations.
  * Currently contains:
@@ -72,6 +89,10 @@
             new Pair(MacAddress.BROADCAST_ADDRESS, MacAddress.BROADCAST_ADDRESS);
     private static final Pair<MacAddress, MacAddress> MATCH_ALL_BSSID_PATTERN =
             new Pair(MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS);
+    private static final String MAC_RANDOMIZATION_ALIAS = "MacRandSecret";
+    private static final long MAC_ADDRESS_VALID_LONG_MASK = (1L << 48) - 1;
+    private static final long MAC_ADDRESS_LOCALLY_ASSIGNED_MASK = 1L << 41;
+    private static final long MAC_ADDRESS_MULTICAST_MASK = 1L << 40;
 
     /**
      * Check whether a network configuration is visible to a user or any of its managed profiles.
@@ -227,6 +248,87 @@
     }
 
     /**
+     * Computes the persistent randomized MAC of the given configuration using the given
+     * hash function.
+     * @param config the WifiConfiguration to compute MAC address for
+     * @param hashFunction the hash function that will perform the MAC address computation.
+     * @return The persistent randomized MAC address or null if inputs are invalid.
+     */
+    public static MacAddress calculatePersistentMacForConfiguration(WifiConfiguration config,
+            Mac hashFunction) {
+        if (config == null || hashFunction == null) {
+            return null;
+        }
+        byte[] hashedBytes = hashFunction.doFinal(
+                config.getSsidAndSecurityTypeString().getBytes(StandardCharsets.UTF_8));
+        ByteBuffer bf = ByteBuffer.wrap(hashedBytes);
+        long longFromSsid = bf.getLong();
+        /**
+         * Masks the generated long so that it represents a valid randomized MAC address.
+         * Specifically, this sets the locally assigned bit to 1, multicast bit to 0
+         */
+        longFromSsid &= MAC_ADDRESS_VALID_LONG_MASK;
+        longFromSsid |= MAC_ADDRESS_LOCALLY_ASSIGNED_MASK;
+        longFromSsid &= ~MAC_ADDRESS_MULTICAST_MASK;
+        bf.clear();
+        bf.putLong(0, longFromSsid);
+
+        // MacAddress.fromBytes requires input of length 6, which is obtained from the
+        // last 6 bytes from the generated long.
+        MacAddress macAddress = MacAddress.fromBytes(Arrays.copyOfRange(bf.array(), 2, 8));
+        return macAddress;
+    }
+
+    /**
+     * Retrieves a Hash function that could be used to calculate the persistent randomized MAC
+     * for a WifiConfiguration.
+     * @param uid the UID of the KeyStore to get the secret of the hash function from.
+     */
+    public static Mac obtainMacRandHashFunction(int uid) {
+        try {
+            KeyStore keyStore = AndroidKeyStoreProvider.getKeyStoreForUid(uid);
+            // tries to retrieve the secret, and generate a new one if it's unavailable.
+            Key key = keyStore.getKey(MAC_RANDOMIZATION_ALIAS, null);
+            if (key == null) {
+                key = generateAndPersistNewMacRandomizationSecret(uid);
+            }
+            if (key == null) {
+                Log.e(TAG, "Failed to generate secret for " + MAC_RANDOMIZATION_ALIAS);
+                return null;
+            }
+            Mac result = Mac.getInstance("HmacSHA256");
+            result.init(key);
+            return result;
+        } catch (KeyStoreException | NoSuchAlgorithmException | InvalidKeyException
+                | UnrecoverableKeyException | NoSuchProviderException e) {
+            Log.e(TAG, "Failure in obtainMacRandHashFunction", e);
+            return null;
+        }
+    }
+
+    /**
+     * Generates and returns a secret key to use for Mac randomization.
+     * Will also persist the generated secret inside KeyStore, accessible in the
+     * future with KeyGenerator#getKey.
+     */
+    private static SecretKey generateAndPersistNewMacRandomizationSecret(int uid) {
+        try {
+            KeyGenerator keyGenerator = KeyGenerator.getInstance(
+                    KeyProperties.KEY_ALGORITHM_HMAC_SHA256, "AndroidKeyStore");
+            keyGenerator.init(
+                    new KeyGenParameterSpec.Builder(MAC_RANDOMIZATION_ALIAS,
+                            KeyProperties.PURPOSE_SIGN)
+                            .setUid(uid)
+                            .build());
+            return keyGenerator.generateKey();
+        } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
+                | NoSuchProviderException | ProviderException e) {
+            Log.e(TAG, "Failure in generateMacRandomizationSecret", e);
+            return null;
+        }
+    }
+
+    /**
      * Compare existing and new WifiEnterpriseConfig objects after a network update and return if
      * credential parameters have changed or not.
      *
diff --git a/tests/wifitests/src/com/android/server/wifi/WifiConfigManagerTest.java b/tests/wifitests/src/com/android/server/wifi/WifiConfigManagerTest.java
index 686b209..0badc6f 100644
--- a/tests/wifitests/src/com/android/server/wifi/WifiConfigManagerTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/WifiConfigManagerTest.java
@@ -63,6 +63,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -111,6 +112,8 @@
     private static final int TEST_FREQUENCY_1 = 2412;
     private static final int TEST_FREQUENCY_2 = 5180;
     private static final int TEST_FREQUENCY_3 = 5240;
+    private static final MacAddress TEST_RANDOMIZED_MAC =
+            MacAddress.fromString("d2:11:19:34:a5:20");
 
     @Mock private Context mContext;
     @Mock private Clock mClock;
@@ -228,9 +231,13 @@
         // static mocking
         mSession = ExtendedMockito.mockitoSession()
                 .mockStatic(WifiConfigStore.class, withSettings().lenient())
+                .spyStatic(WifiConfigurationUtil.class)
+                .strictness(Strictness.LENIENT)
                 .startMocking();
         when(WifiConfigStore.createUserFiles(anyInt())).thenReturn(mock(List.class));
         when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mDataTelephonyManager);
+        when(WifiConfigurationUtil.calculatePersistentMacForConfiguration(any(), any()))
+                .thenReturn(TEST_RANDOMIZED_MAC);
     }
 
     /**
@@ -239,7 +246,9 @@
     @After
     public void cleanup() {
         validateMockitoUsage();
-        mSession.finishMocking();
+        if (mSession != null) {
+            mSession.finishMocking();
+        }
     }
 
     /**
@@ -368,6 +377,7 @@
      */
     @Test
     public void testAddingNetworkWithMatchingMacAddressOverridesField() {
+        int prevMappingSize = mWifiConfigManager.getRandomizedMacAddressMappingSize();
         WifiConfiguration openNetwork = WifiConfigurationTestUtil.createOpenNetwork();
         Map<String, String> randomizedMacAddressMapping = new HashMap<>();
         final String randMac = "12:23:34:45:56:67";
@@ -382,6 +392,9 @@
         List<WifiConfiguration> retrievedNetworks =
                 mWifiConfigManager.getConfiguredNetworksWithPasswords();
         assertEquals(randMac, retrievedNetworks.get(0).getRandomizedMacAddress().toString());
+        // Verify that for networks that we already have randomizedMacAddressMapping saved
+        // we are still correctly writing into the WifiConfigStore.
+        assertEquals(prevMappingSize + 1, mWifiConfigManager.getRandomizedMacAddressMappingSize());
     }
 
     /**
@@ -391,6 +404,7 @@
      */
     @Test
     public void testRandomizedMacAddressIsPersistedOverForgetNetwork() {
+        int prevMappingSize = mWifiConfigManager.getRandomizedMacAddressMappingSize();
         // Create and add an open network
         WifiConfiguration openNetwork = WifiConfigurationTestUtil.createOpenNetwork();
         verifyAddNetworkToWifiConfigManager(openNetwork);
@@ -410,6 +424,8 @@
         verifyAddNetworkToWifiConfigManager(openNetwork);
         retrievedNetworks = mWifiConfigManager.getConfiguredNetworksWithPasswords();
         assertEquals(randMac, retrievedNetworks.get(0).getRandomizedMacAddress().toString());
+        // Verify that we are no longer persisting the randomized MAC address with WifiConfigStore.
+        assertEquals(prevMappingSize, mWifiConfigManager.getRandomizedMacAddressMappingSize());
     }
 
     /**
diff --git a/tests/wifitests/src/com/android/server/wifi/WifiConfigurationUtilTest.java b/tests/wifitests/src/com/android/server/wifi/WifiConfigurationUtilTest.java
index c1640ce..7173dae 100644
--- a/tests/wifitests/src/com/android/server/wifi/WifiConfigurationUtilTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/WifiConfigurationUtilTest.java
@@ -25,6 +25,7 @@
 import android.net.wifi.WifiEnterpriseConfig;
 import android.net.wifi.WifiNetworkSpecifier;
 import android.net.wifi.WifiScanner;
+import android.os.Binder;
 import android.os.PatternMatcher;
 import android.os.UserHandle;
 import android.util.Pair;
@@ -39,6 +40,8 @@
 import java.util.Collections;
 import java.util.List;
 
+import javax.crypto.Mac;
+
 /**
  * Unit tests for {@link com.android.server.wifi.WifiConfigurationUtil}.
  */
@@ -961,6 +964,33 @@
                 existingConfig, newConfig));
     }
 
+    /**
+     * Verifies that calculatePersistentMacForConfiguration produces persistent, locally generated
+     * MAC addresses that are valid for MAC randomization.
+     */
+    @Test
+    public void testCalculatePersistentMacForConfiguration() {
+        // verify null inputs
+        assertNull(WifiConfigurationUtil.calculatePersistentMacForConfiguration(null, null));
+
+        // test multiple times since there is some randomness involved with hashing
+        int uid = Binder.getCallingUid();
+        for (int i = 0; i < 10; i++) {
+            // Verify that a the MAC address calculated is valid
+            WifiConfiguration config = WifiConfigurationTestUtil.createOpenNetwork();
+            Mac hashFunction = WifiConfigurationUtil.obtainMacRandHashFunction(uid);
+            MacAddress macAddress = WifiConfigurationUtil.calculatePersistentMacForConfiguration(
+                    config, hashFunction);
+            assertTrue(WifiConfiguration.isValidMacAddressForRandomization(macAddress));
+
+            // Verify that the secret used to generate MAC address is persistent
+            Mac hashFunction2 = WifiConfigurationUtil.obtainMacRandHashFunction(uid);
+            MacAddress macAddress2 = WifiConfigurationUtil.calculatePersistentMacForConfiguration(
+                    config, hashFunction2);
+            assertEquals(macAddress, macAddress2);
+        }
+    }
+
     private static class EnterpriseConfig {
         public String eap;
         public String phase2;