1 /* 2 * Copyright (C) 2013 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 package com.android.nfc.cardemulation; 17 18 import android.sysprop.NfcProperties; 19 import android.util.Log; 20 import android.util.SparseArray; 21 import android.util.proto.ProtoOutputStream; 22 23 import com.android.nfc.NfcService; 24 import com.android.nfc.NfcStatsLog; 25 26 import java.io.FileDescriptor; 27 import java.io.PrintWriter; 28 import java.util.ArrayList; 29 import java.util.Arrays; 30 import java.util.HashMap; 31 import java.util.HashSet; 32 import java.util.Map; 33 import java.util.Set; 34 35 public class AidRoutingManager { 36 37 static final String TAG = "AidRoutingManager"; 38 39 static final boolean DBG = NfcProperties.debug_enabled().orElse(false); 40 41 static final int ROUTE_HOST = 0x00; 42 43 // Every routing table entry is matched exact 44 static final int AID_MATCHING_EXACT_ONLY = 0x00; 45 // Every routing table entry can be matched either exact or prefix 46 static final int AID_MATCHING_EXACT_OR_PREFIX = 0x01; 47 // Every routing table entry is matched as a prefix 48 static final int AID_MATCHING_PREFIX_ONLY = 0x02; 49 // Every routing table entry can be matched either exact or prefix or subset only 50 static final int AID_MATCHING_EXACT_OR_SUBSET_OR_PREFIX = 0x03; 51 52 int mDefaultIsoDepRoute; 53 //Let mDefaultRoute as default aid route 54 int mDefaultRoute; 55 56 int mMaxAidRoutingTableSize; 57 58 final byte[] mOffHostRouteUicc; 59 final byte[] mOffHostRouteEse; 60 // Used for backward compatibility in case application doesn't specify the 61 // SE 62 final int mDefaultOffHostRoute; 63 64 // How the NFC controller can match AIDs in the routing table; 65 // see AID_MATCHING constants 66 final int mAidMatchingSupport; 67 68 final Object mLock = new Object(); 69 70 // mAidRoutingTable contains the current routing table. The index is the route ID. 71 // The route can include routes to a eSE/UICC. 72 SparseArray<Set<String>> mAidRoutingTable = 73 new SparseArray<Set<String>>(); 74 75 // Easy look-up what the route is for a certain AID 76 HashMap<String, Integer> mRouteForAid = new HashMap<String, Integer>(); 77 doGetDefaultRouteDestination()78 private native int doGetDefaultRouteDestination(); doGetDefaultOffHostRouteDestination()79 private native int doGetDefaultOffHostRouteDestination(); doGetOffHostUiccDestination()80 private native byte[] doGetOffHostUiccDestination(); doGetOffHostEseDestination()81 private native byte[] doGetOffHostEseDestination(); doGetAidMatchingMode()82 private native int doGetAidMatchingMode(); doGetDefaultIsoDepRouteDestination()83 private native int doGetDefaultIsoDepRouteDestination(); 84 85 final class AidEntry { 86 boolean isOnHost; 87 String offHostSE; 88 int route; 89 int aidInfo; 90 int power; 91 } 92 AidRoutingManager()93 public AidRoutingManager() { 94 mDefaultRoute = doGetDefaultRouteDestination(); 95 if (DBG) 96 Log.d(TAG, "mDefaultRoute=0x" + Integer.toHexString(mDefaultRoute)); 97 mDefaultOffHostRoute = doGetDefaultOffHostRouteDestination(); 98 if (DBG) 99 Log.d(TAG, "mDefaultOffHostRoute=0x" + Integer.toHexString(mDefaultOffHostRoute)); 100 mOffHostRouteUicc = doGetOffHostUiccDestination(); 101 if (DBG) 102 Log.d(TAG, "mOffHostRouteUicc=" + Arrays.toString(mOffHostRouteUicc)); 103 mOffHostRouteEse = doGetOffHostEseDestination(); 104 if (DBG) 105 Log.d(TAG, "mOffHostRouteEse=" + Arrays.toString(mOffHostRouteEse)); 106 mAidMatchingSupport = doGetAidMatchingMode(); 107 if (DBG) Log.d(TAG, "mAidMatchingSupport=0x" + Integer.toHexString(mAidMatchingSupport)); 108 109 mDefaultIsoDepRoute = doGetDefaultIsoDepRouteDestination(); 110 if (DBG) Log.d(TAG, "mDefaultIsoDepRoute=0x" + Integer.toHexString(mDefaultIsoDepRoute)); 111 } 112 supportsAidPrefixRouting()113 public boolean supportsAidPrefixRouting() { 114 return mAidMatchingSupport == AID_MATCHING_EXACT_OR_PREFIX || 115 mAidMatchingSupport == AID_MATCHING_PREFIX_ONLY || 116 mAidMatchingSupport == AID_MATCHING_EXACT_OR_SUBSET_OR_PREFIX; 117 } 118 supportsAidSubsetRouting()119 public boolean supportsAidSubsetRouting() { 120 return mAidMatchingSupport == AID_MATCHING_EXACT_OR_SUBSET_OR_PREFIX; 121 } 122 calculateAidRouteSize(HashMap<String, AidEntry> routeCache)123 public int calculateAidRouteSize(HashMap<String, AidEntry> routeCache) { 124 // TAG + ROUTE + LENGTH_BYTE + POWER 125 int AID_HDR_LENGTH = 0x04; 126 int routeTableSize = 0x00; 127 for(Map.Entry<String, AidEntry> aidEntry : routeCache.entrySet()) { 128 String aid = aidEntry.getKey(); 129 // removing prefix length 130 if(aid.endsWith("*")) { 131 routeTableSize += ((aid.length() - 0x01) / 0x02) + AID_HDR_LENGTH; 132 } else { 133 routeTableSize += (aid.length() / 0x02)+ AID_HDR_LENGTH; 134 } 135 } 136 if (DBG) Log.d(TAG, "calculateAidRouteSize: " + routeTableSize); 137 return routeTableSize; 138 } 139 clearNfcRoutingTableLocked()140 private void clearNfcRoutingTableLocked() { 141 for (Map.Entry<String, Integer> aidEntry : mRouteForAid.entrySet()) { 142 String aid = aidEntry.getKey(); 143 if (aid.endsWith("*")) { 144 if (mAidMatchingSupport == AID_MATCHING_EXACT_ONLY) { 145 Log.e(TAG, "Device does not support prefix AIDs but AID [" + aid 146 + "] is registered"); 147 } else if (mAidMatchingSupport == AID_MATCHING_PREFIX_ONLY) { 148 if (DBG) Log.d(TAG, "Unrouting prefix AID " + aid); 149 // Cut off '*' since controller anyway treats all AIDs as a prefix 150 aid = aid.substring(0, aid.length() - 1); 151 } else if (mAidMatchingSupport == AID_MATCHING_EXACT_OR_PREFIX || 152 mAidMatchingSupport == AID_MATCHING_EXACT_OR_SUBSET_OR_PREFIX) { 153 aid = aid.substring(0, aid.length() - 1); 154 if (DBG) Log.d(TAG, "Unrouting prefix AID " + aid); 155 } 156 } else if (aid.endsWith("#")) { 157 if (mAidMatchingSupport == AID_MATCHING_EXACT_ONLY) { 158 Log.e(TAG, "Device does not support subset AIDs but AID [" + aid 159 + "] is registered"); 160 } else if (mAidMatchingSupport == AID_MATCHING_PREFIX_ONLY || 161 mAidMatchingSupport == AID_MATCHING_EXACT_OR_PREFIX) { 162 Log.e(TAG, "Device does not support subset AIDs but AID [" + aid 163 + "] is registered"); 164 } else if (mAidMatchingSupport == AID_MATCHING_EXACT_OR_SUBSET_OR_PREFIX) { 165 if (DBG) Log.d(TAG, "Unrouting subset AID " + aid); 166 aid = aid.substring(0, aid.length() - 1); 167 } 168 } else { 169 if (DBG) Log.d(TAG, "Unrouting exact AID " + aid); 170 } 171 172 NfcService.getInstance().unrouteAids(aid); 173 } 174 if (NfcService.getInstance().getNciVersion() >= NfcService.getInstance().NCI_VERSION_2_0) { 175 // unRoute EmptyAid 176 NfcService.getInstance().unrouteAids(""); 177 } 178 } 179 getRouteForSecureElement(String se)180 private int getRouteForSecureElement(String se) { 181 if (se == null || se.length() <= 3) { 182 return 0; 183 } 184 try { 185 if (se.startsWith("eSE") && mOffHostRouteEse != null) { 186 int index = Integer.parseInt(se.substring(3)); 187 if (mOffHostRouteEse.length >= index && index > 0) { 188 return mOffHostRouteEse[index - 1] & 0xFF; 189 } 190 } else if (se.startsWith("SIM") && mOffHostRouteUicc != null) { 191 int index = Integer.parseInt(se.substring(3)); 192 if (mOffHostRouteUicc.length >= index && index > 0) { 193 return mOffHostRouteUicc[index - 1] & 0xFF; 194 } 195 } 196 if (mOffHostRouteEse == null && mOffHostRouteUicc == null) 197 return mDefaultOffHostRoute; 198 } catch (NumberFormatException e) { } 199 return 0; 200 } 201 configureRouting(HashMap<String, AidEntry> aidMap, boolean force)202 public boolean configureRouting(HashMap<String, AidEntry> aidMap, boolean force) { 203 boolean aidRouteResolved = false; 204 HashMap<String, AidEntry> aidRoutingTableCache = new HashMap<String, AidEntry>(aidMap.size()); 205 ArrayList<Integer> seList = new ArrayList<Integer>(); 206 mDefaultRoute = doGetDefaultRouteDestination(); 207 seList.add(mDefaultRoute); 208 if (mDefaultRoute != ROUTE_HOST) { 209 seList.add(ROUTE_HOST); 210 } 211 212 SparseArray<Set<String>> aidRoutingTable = new SparseArray<Set<String>>(aidMap.size()); 213 HashMap<String, Integer> routeForAid = new HashMap<String, Integer>(aidMap.size()); 214 HashMap<String, Integer> infoForAid = new HashMap<String, Integer>(aidMap.size()); 215 // Then, populate internal data structures first 216 for (Map.Entry<String, AidEntry> aidEntry : aidMap.entrySet()) { 217 int route = ROUTE_HOST; 218 if (!aidEntry.getValue().isOnHost) { 219 String offHostSE = aidEntry.getValue().offHostSE; 220 if (offHostSE == null) { 221 route = mDefaultOffHostRoute; 222 } else { 223 route = getRouteForSecureElement(offHostSE); 224 if (route == 0) { 225 Log.e(TAG, "Invalid Off host Aid Entry " + offHostSE); 226 continue; 227 } 228 } 229 } 230 if (!seList.contains(route)) 231 seList.add(route); 232 aidEntry.getValue().route = route; 233 int aidType = aidEntry.getValue().aidInfo; 234 String aid = aidEntry.getKey(); 235 Set<String> entries = 236 aidRoutingTable.get(route, new HashSet<String>()); 237 entries.add(aid); 238 aidRoutingTable.put(route, entries); 239 routeForAid.put(aid, route); 240 infoForAid.put(aid, aidType); 241 } 242 243 synchronized (mLock) { 244 if (routeForAid.equals(mRouteForAid) && !force) { 245 if (DBG) Log.d(TAG, "Routing table unchanged, not updating"); 246 return false; 247 } 248 249 // Otherwise, update internal structures and commit new routing 250 clearNfcRoutingTableLocked(); 251 mRouteForAid = routeForAid; 252 mAidRoutingTable = aidRoutingTable; 253 254 mMaxAidRoutingTableSize = NfcService.getInstance().getAidRoutingTableSize(); 255 if (DBG) Log.d(TAG, "mMaxAidRoutingTableSize: " + mMaxAidRoutingTableSize); 256 257 //calculate AidRoutingTableSize for existing route destination 258 for(int index = 0; index < seList.size(); index ++) { 259 mDefaultRoute = seList.get(index); 260 if(index != 0) { 261 if (DBG) { 262 Log.d(TAG, "AidRoutingTable is full, try to switch mDefaultRoute to 0x" + Integer.toHexString(mDefaultRoute)); 263 } 264 } 265 266 aidRoutingTableCache.clear(); 267 268 if (mAidMatchingSupport == AID_MATCHING_PREFIX_ONLY) { 269 /* If a non-default route registers an exact AID which is shorter 270 * than this exact AID, this will create a problem with controllers 271 * that treat every AID in the routing table as a prefix. 272 * For example, if App A registers F0000000041010 as an exact AID, 273 * and App B registers F000000004 as an exact AID, and App B is not 274 * the default route, the following would be added to the routing table: 275 * F000000004 -> non-default destination 276 * However, because in this mode, the controller treats every routing table 277 * entry as a prefix, it means F0000000041010 would suddenly go to the non-default 278 * destination too, whereas it should have gone to the default. 279 * 280 * The only way to prevent this is to add the longer AIDs of the 281 * default route at the top of the table, so they will be matched first. 282 */ 283 Set<String> defaultRouteAids = mAidRoutingTable.get(mDefaultRoute); 284 if (defaultRouteAids != null) { 285 for (String defaultRouteAid : defaultRouteAids) { 286 // Check whether there are any shorted AIDs routed to non-default 287 // TODO this is O(N^2) run-time complexity... 288 for (Map.Entry<String, Integer> aidEntry : mRouteForAid.entrySet()) { 289 String aid = aidEntry.getKey(); 290 int route = aidEntry.getValue(); 291 if (defaultRouteAid.startsWith(aid) && route != mDefaultRoute) { 292 if (DBG) Log.d(TAG, "Adding AID " + defaultRouteAid + " for default " + 293 "route, because a conflicting shorter AID will be " + 294 "added to the routing table"); 295 aidRoutingTableCache.put(defaultRouteAid, aidMap.get(defaultRouteAid)); 296 } 297 } 298 } 299 } 300 } 301 302 // Add AID entries for all non-default routes 303 for (int i = 0; i < mAidRoutingTable.size(); i++) { 304 int route = mAidRoutingTable.keyAt(i); 305 if (route != mDefaultRoute) { 306 Set<String> aidsForRoute = mAidRoutingTable.get(route); 307 for (String aid : aidsForRoute) { 308 if (aid.endsWith("*")) { 309 if (mAidMatchingSupport == AID_MATCHING_EXACT_ONLY) { 310 Log.e(TAG, "This device does not support prefix AIDs."); 311 } else if (mAidMatchingSupport == AID_MATCHING_PREFIX_ONLY) { 312 if (DBG) Log.d(TAG, "Routing prefix AID " + aid + " to route " 313 + Integer.toString(route)); 314 // Cut off '*' since controller anyway treats all AIDs as a prefix 315 aidRoutingTableCache.put(aid.substring(0,aid.length() - 1), aidMap.get(aid)); 316 } else if (mAidMatchingSupport == AID_MATCHING_EXACT_OR_PREFIX || 317 mAidMatchingSupport == AID_MATCHING_EXACT_OR_SUBSET_OR_PREFIX) { 318 if (DBG) Log.d(TAG, "Routing prefix AID " + aid + " to route " 319 + Integer.toString(route)); 320 aidRoutingTableCache.put(aid.substring(0,aid.length() - 1), aidMap.get(aid)); 321 } 322 } else if (aid.endsWith("#")) { 323 if (mAidMatchingSupport == AID_MATCHING_EXACT_ONLY) { 324 Log.e(TAG, "Device does not support subset AIDs but AID [" + aid 325 + "] is registered"); 326 } else if (mAidMatchingSupport == AID_MATCHING_PREFIX_ONLY || 327 mAidMatchingSupport == AID_MATCHING_EXACT_OR_PREFIX) { 328 Log.e(TAG, "Device does not support subset AIDs but AID [" + aid 329 + "] is registered"); 330 } else if (mAidMatchingSupport == AID_MATCHING_EXACT_OR_SUBSET_OR_PREFIX) { 331 if (DBG) Log.d(TAG, "Routing subset AID " + aid + " to route " 332 + Integer.toString(route)); 333 aidRoutingTableCache.put(aid.substring(0,aid.length() - 1), aidMap.get(aid)); 334 } 335 } else { 336 if (DBG) Log.d(TAG, "Routing exact AID " + aid + " to route " 337 + Integer.toString(route)); 338 aidRoutingTableCache.put(aid, aidMap.get(aid)); 339 } 340 } 341 } 342 } 343 344 // register default route in below cases: 345 // 1. mDefaultRoute is different with mDefaultIsoDepRoute 346 // 2. mDefaultRoute and mDefaultIsoDepRoute all equal to ROUTE_HOST 347 // , which is used for screen off HCE scenarios 348 if (mDefaultRoute != mDefaultIsoDepRoute || mDefaultIsoDepRoute == ROUTE_HOST) { 349 if (NfcService.getInstance().getNciVersion() 350 >= NfcService.getInstance().NCI_VERSION_2_0) { 351 String emptyAid = ""; 352 AidEntry entry = new AidEntry(); 353 int default_route_power_state; 354 entry.route = mDefaultRoute; 355 if (mDefaultRoute == ROUTE_HOST) { 356 entry.isOnHost = true; 357 default_route_power_state = RegisteredAidCache.POWER_STATE_SWITCH_ON 358 | RegisteredAidCache.POWER_STATE_SCREEN_ON_LOCKED; 359 Set<String> aidsForDefaultRoute = mAidRoutingTable.get(mDefaultRoute); 360 if (aidsForDefaultRoute != null) { 361 for (String aid : aidsForDefaultRoute) { 362 default_route_power_state |= aidMap.get(aid).power; 363 } 364 } 365 } else { 366 entry.isOnHost = false; 367 default_route_power_state = RegisteredAidCache.POWER_STATE_ALL; 368 } 369 entry.aidInfo = RegisteredAidCache.AID_ROUTE_QUAL_PREFIX; 370 entry.power = default_route_power_state; 371 372 aidRoutingTableCache.put(emptyAid, entry); 373 if (DBG) Log.d(TAG, "Add emptyAid into AidRoutingTable"); 374 } 375 } 376 377 // Register additional offhost AIDs when their support power states are 378 // differernt from the default route entry 379 if (mDefaultRoute != ROUTE_HOST) { 380 int default_route_power_state = RegisteredAidCache.POWER_STATE_ALL; 381 if (NfcService.getInstance().getNciVersion() 382 < NfcService.getInstance().NCI_VERSION_2_0) { 383 default_route_power_state = 384 RegisteredAidCache.POWER_STATE_ALL_NCI_VERSION_1_0; 385 } 386 387 Set<String> aidsForDefaultRoute = mAidRoutingTable.get(mDefaultRoute); 388 if (aidsForDefaultRoute != null) { 389 for (String aid : aidsForDefaultRoute) { 390 if (aidMap.get(aid).power != default_route_power_state) { 391 aidRoutingTableCache.put(aid, aidMap.get(aid)); 392 } 393 } 394 } 395 } 396 397 if (calculateAidRouteSize(aidRoutingTableCache) <= mMaxAidRoutingTableSize) { 398 aidRouteResolved = true; 399 break; 400 } 401 } 402 403 if(aidRouteResolved == true) { 404 commit(aidRoutingTableCache); 405 } else { 406 NfcStatsLog.write(NfcStatsLog.NFC_ERROR_OCCURRED, 407 NfcStatsLog.NFC_ERROR_OCCURRED__TYPE__AID_OVERFLOW, 0, 0); 408 Log.e(TAG, "RoutingTable unchanged because it's full, not updating"); 409 } 410 } 411 return true; 412 } 413 commit(HashMap<String, AidEntry> routeCache )414 private void commit(HashMap<String, AidEntry> routeCache ) { 415 416 if(routeCache != null) { 417 418 for (Map.Entry<String, AidEntry> aidEntry : routeCache.entrySet()) { 419 int route = aidEntry.getValue().route; 420 int aidType = aidEntry.getValue().aidInfo; 421 String aid = aidEntry.getKey(); 422 int power = aidEntry.getValue().power; 423 if (DBG) { 424 Log.d(TAG, "commit aid:" + aid + ",route:" + route 425 + ",aidtype:" + aidType + ", power state:" + power); 426 } 427 428 NfcService.getInstance().routeAids(aid, route, aidType, power); 429 } 430 } 431 432 // And finally commit the routing 433 NfcService.getInstance().commitRouting(); 434 } 435 436 /** 437 * This notifies that the AID routing table in the controller 438 * has been cleared (usually due to NFC being turned off). 439 */ onNfccRoutingTableCleared()440 public void onNfccRoutingTableCleared() { 441 // The routing table in the controller was cleared 442 // To stay in sync, clear our own tables. 443 synchronized (mLock) { 444 mAidRoutingTable.clear(); 445 mRouteForAid.clear(); 446 } 447 } 448 dump(FileDescriptor fd, PrintWriter pw, String[] args)449 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 450 pw.println("Routing table:"); 451 pw.println(" Default route: " + ((mDefaultRoute == 0x00) ? "host" : "secure element")); 452 synchronized (mLock) { 453 for (int i = 0; i < mAidRoutingTable.size(); i++) { 454 Set<String> aids = mAidRoutingTable.valueAt(i); 455 pw.println(" Routed to 0x" + Integer.toHexString(mAidRoutingTable.keyAt(i)) + ":"); 456 for (String aid : aids) { 457 pw.println(" \"" + aid + "\""); 458 } 459 } 460 } 461 } 462 463 /** 464 * Dump debugging information as a AidRoutingManagerProto 465 * 466 * Note: 467 * See proto definition in frameworks/base/core/proto/android/nfc/card_emulation.proto 468 * When writing a nested message, must call {@link ProtoOutputStream#start(long)} before and 469 * {@link ProtoOutputStream#end(long)} after. 470 * Never reuse a proto field number. When removing a field, mark it as reserved. 471 */ dumpDebug(ProtoOutputStream proto)472 void dumpDebug(ProtoOutputStream proto) { 473 proto.write(AidRoutingManagerProto.DEFAULT_ROUTE, mDefaultRoute); 474 synchronized (mLock) { 475 for (int i = 0; i < mAidRoutingTable.size(); i++) { 476 long token = proto.start(AidRoutingManagerProto.ROUTES); 477 proto.write(AidRoutingManagerProto.Route.ID, mAidRoutingTable.keyAt(i)); 478 mAidRoutingTable.valueAt(i).forEach(aid -> { 479 proto.write(AidRoutingManagerProto.Route.AIDS, aid); 480 }); 481 proto.end(token); 482 } 483 } 484 } 485 } 486