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