1 /* 2 * Copyright (C) 2022 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.net.MacAddress; 20 import android.net.wifi.WifiConfiguration; 21 import android.os.Handler; 22 import android.util.Log; 23 import android.util.SparseArray; 24 25 import com.android.internal.annotations.GuardedBy; 26 import com.android.internal.annotations.VisibleForTesting; 27 28 import java.util.ArrayList; 29 import java.util.List; 30 import java.util.Objects; 31 32 /** Utilities for storing PMK cache. */ 33 public class PmkCacheManager { 34 private static final String TAG = "PmkCacheManager"; 35 36 @VisibleForTesting 37 static final String PMK_CACHE_EXPIRATION_ALARM_TAG = "PMK_CACHE_EXPIRATION_TIMER"; 38 39 private final Clock mClock; 40 private final Handler mEventHandler; 41 42 private boolean mVerboseLoggingEnabled = false; 43 44 private final Object mLock = new Object(); 45 46 @GuardedBy("mLock") 47 private SparseArray<List<PmkCacheStoreData>> mPmkCacheEntries = new SparseArray<>(); 48 PmkCacheManager(Clock clock, Handler eventHandler)49 public PmkCacheManager(Clock clock, Handler eventHandler) { 50 mClock = clock; 51 mEventHandler = eventHandler; 52 } 53 54 /** 55 * Add a PMK cache entry to the store. 56 * 57 * @param macAddress the interface MAC address to connect to the network. 58 * @param networkId the network ID of the WifiConfiguration associates with the network. 59 * @param bssid BSSID of the access point to which the station is associated 60 * @param expirationTimeInSec the expiration time of the PMK cache since boot. 61 * @param serializedEntry the opaque data of the PMK cache. 62 * @return true when PMK cache is added; otherwise, false. 63 */ add(MacAddress macAddress, int networkId, MacAddress bssid, long expirationTimeInSec, ArrayList<Byte> serializedEntry)64 public boolean add(MacAddress macAddress, int networkId, MacAddress bssid, 65 long expirationTimeInSec, ArrayList<Byte> serializedEntry) { 66 synchronized (mLock) { 67 if (WifiConfiguration.INVALID_NETWORK_ID == networkId) return false; 68 if (macAddress == null) { 69 Log.w(TAG, "Omit PMK cache due to no valid MAC address"); 70 return false; 71 } 72 if (null == serializedEntry) { 73 Log.w(TAG, "Omit PMK cache due to null entry."); 74 return false; 75 } 76 final long elapseTimeInSecond = mClock.getElapsedSinceBootMillis() / 1000; 77 if (elapseTimeInSecond >= expirationTimeInSec) { 78 Log.w(TAG, "Omit expired PMK cache."); 79 return false; 80 } 81 82 PmkCacheStoreData newStoreData = 83 new PmkCacheStoreData(macAddress, bssid, serializedEntry, expirationTimeInSec); 84 List<PmkCacheStoreData> pmkDataList = mPmkCacheEntries.get(networkId); 85 if (pmkDataList == null) { 86 pmkDataList = new ArrayList<>(); 87 mPmkCacheEntries.put(networkId, pmkDataList); 88 } else { 89 PmkCacheStoreData existStoreData = null; 90 if (bssid != null) { 91 // Remove the stored PMK cache if the PMK cache is changed for an existing 92 // BSSID. 93 for (PmkCacheStoreData storeData : pmkDataList) { 94 if (Objects.equals(storeData.bssid, bssid)) { 95 existStoreData = storeData; 96 break; 97 } 98 } 99 if (null != existStoreData) { 100 if (Objects.equals(existStoreData, newStoreData)) { 101 if (mVerboseLoggingEnabled) { 102 Log.d(TAG, "PMK entry exists for the BSSID, skip it."); 103 } 104 return true; 105 } 106 pmkDataList.remove(existStoreData); 107 } 108 } else { 109 for (PmkCacheStoreData storeData : pmkDataList) { 110 if (Objects.equals(storeData, newStoreData)) { 111 existStoreData = storeData; 112 break; 113 } 114 } 115 if (null != existStoreData) { 116 if (mVerboseLoggingEnabled) { 117 Log.d(TAG, "PMK entry exists, skip it."); 118 } 119 return true; 120 } 121 } 122 } 123 124 pmkDataList.add(newStoreData); 125 if (mVerboseLoggingEnabled) { 126 Log.d(TAG, "Network " + networkId + " PmkCache Count: " + pmkDataList.size()); 127 } 128 updatePmkCacheExpiration(); 129 return true; 130 } 131 } 132 133 /** 134 * Remove PMK caches associated with the network ID. 135 * 136 * @param networkId the network ID of PMK caches to be removed. 137 * @return true when PMK caches are removed; otherwise, false. 138 */ remove(int networkId)139 public boolean remove(int networkId) { 140 synchronized (mLock) { 141 if (WifiConfiguration.INVALID_NETWORK_ID == networkId) return false; 142 if (!mPmkCacheEntries.contains(networkId)) return false; 143 144 mPmkCacheEntries.remove(networkId); 145 updatePmkCacheExpiration(); 146 return true; 147 } 148 } 149 150 /** 151 * Remove PMK caches associated with the network ID when the interface 152 * MAC address is changed. 153 * 154 * @param networkId the network ID of PMK caches to be removed. 155 * @param curMacAddress current interface MAC address. 156 * @return true when PMK caches are removed; otherwise, false. 157 */ 158 remove(int networkId, MacAddress curMacAddress)159 public boolean remove(int networkId, MacAddress curMacAddress) { 160 synchronized (mLock) { 161 if (WifiConfiguration.INVALID_NETWORK_ID == networkId) return false; 162 List<PmkCacheStoreData> pmkDataList = mPmkCacheEntries.get(networkId); 163 if (null == pmkDataList) return false; 164 165 pmkDataList.removeIf(pmkData -> !Objects.equals(curMacAddress, pmkData.macAddress)); 166 167 if (pmkDataList.size() == 0) { 168 remove(networkId); 169 } 170 return true; 171 } 172 } 173 174 /** 175 * Get PMK caches associated with the network ID. 176 * 177 * @param networkId the network ID to be queried. 178 * @return A list of PMK caches associated with the network ID. 179 * If none of PMK cache is associated with the network ID, return null. 180 */ get(int networkId)181 public List<ArrayList<Byte>> get(int networkId) { 182 synchronized (mLock) { 183 List<PmkCacheStoreData> pmkDataList = mPmkCacheEntries.get(networkId); 184 if (WifiConfiguration.INVALID_NETWORK_ID == networkId) return null; 185 if (null == pmkDataList) return null; 186 187 final long elapseTimeInSecond = mClock.getElapsedSinceBootMillis() / 1000; 188 List<ArrayList<Byte>> dataList = new ArrayList<>(); 189 for (PmkCacheStoreData pmkData : pmkDataList) { 190 if (pmkData.isValid(elapseTimeInSecond)) { 191 dataList.add(pmkData.data); 192 } 193 } 194 return dataList; 195 } 196 } 197 198 /** 199 * Enable/Disable verbose logging. 200 */ enableVerboseLogging(boolean verboseEnabled)201 public void enableVerboseLogging(boolean verboseEnabled) { 202 mVerboseLoggingEnabled = verboseEnabled; 203 } 204 205 @VisibleForTesting updatePmkCacheExpiration()206 void updatePmkCacheExpiration() { 207 synchronized (mLock) { 208 mEventHandler.removeCallbacksAndMessages(PMK_CACHE_EXPIRATION_ALARM_TAG); 209 210 long elapseTimeInSecond = mClock.getElapsedSinceBootMillis() / 1000; 211 long nextUpdateTimeInSecond = Long.MAX_VALUE; 212 if (mVerboseLoggingEnabled) { 213 Log.d(TAG, "Update PMK cache expiration at " + elapseTimeInSecond); 214 } 215 216 List<Integer> emptyStoreDataList = new ArrayList<>(); 217 for (int i = 0; i < mPmkCacheEntries.size(); i++) { 218 int networkId = mPmkCacheEntries.keyAt(i); 219 List<PmkCacheStoreData> list = mPmkCacheEntries.get(networkId); 220 list.removeIf(pmkData -> !pmkData.isValid(elapseTimeInSecond)); 221 if (list.size() == 0) { 222 emptyStoreDataList.add(networkId); 223 continue; 224 } 225 for (PmkCacheStoreData pmkData : list) { 226 if (nextUpdateTimeInSecond > pmkData.expirationTimeInSec) { 227 nextUpdateTimeInSecond = pmkData.expirationTimeInSec; 228 } 229 } 230 } 231 emptyStoreDataList.forEach(networkId -> mPmkCacheEntries.remove(networkId)); 232 233 // No need to arrange next update since there is no valid PMK in the cache. 234 if (nextUpdateTimeInSecond == Long.MAX_VALUE) { 235 return; 236 } 237 238 if (mVerboseLoggingEnabled) { 239 Log.d(TAG, "PMK cache next expiration time: " + nextUpdateTimeInSecond); 240 } 241 long delayedTimeInMs = (nextUpdateTimeInSecond - elapseTimeInSecond) * 1000; 242 mEventHandler.postDelayed( 243 () -> { 244 updatePmkCacheExpiration(); 245 }, 246 PMK_CACHE_EXPIRATION_ALARM_TAG, 247 (delayedTimeInMs > 0) ? delayedTimeInMs : 0); 248 } 249 } 250 251 private static class PmkCacheStoreData { 252 253 public MacAddress macAddress; 254 public MacAddress bssid; 255 public ArrayList<Byte> data; 256 public long expirationTimeInSec; 257 PmkCacheStoreData(MacAddress macAddr, MacAddress bssAddr, ArrayList<Byte> serializedData, long timeInSec)258 PmkCacheStoreData(MacAddress macAddr, MacAddress bssAddr, ArrayList<Byte> serializedData, 259 long timeInSec) { 260 macAddress = macAddr; 261 bssid = bssAddr; 262 data = serializedData; 263 expirationTimeInSec = timeInSec; 264 } 265 266 /** 267 * Validate this PMK cache against the timestamp. 268 * 269 * @param currentTimeInSec the timestamp to be checked. 270 * @return true if this PMK cache is valid against the timestamp; otherwise, false. 271 */ isValid(long currentTimeInSec)272 public boolean isValid(long currentTimeInSec) { 273 return expirationTimeInSec > 0 && expirationTimeInSec > currentTimeInSec; 274 } 275 276 @Override equals(Object o)277 public boolean equals(Object o) { 278 if (this == o) return true; 279 if (!(o instanceof PmkCacheStoreData)) return false; 280 PmkCacheStoreData storeData = (PmkCacheStoreData) o; 281 return expirationTimeInSec == storeData.expirationTimeInSec 282 && Objects.equals(macAddress, storeData.macAddress) 283 && Objects.equals(data, storeData.data) 284 && Objects.equals(bssid, storeData.bssid); 285 } 286 287 @Override hashCode()288 public int hashCode() { 289 return Objects.hash(macAddress, data, expirationTimeInSec, bssid); 290 } 291 } 292 } 293