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 17 package com.android.nfc.cardemulation; 18 19 import org.xmlpull.v1.XmlPullParser; 20 import org.xmlpull.v1.XmlPullParserException; 21 import org.xmlpull.v1.XmlSerializer; 22 23 import android.app.ActivityManager; 24 import android.content.BroadcastReceiver; 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.IntentFilter; 29 import android.content.pm.PackageManager; 30 import android.content.pm.ResolveInfo; 31 import android.content.pm.ServiceInfo; 32 import android.content.pm.PackageManager.NameNotFoundException; 33 import android.nfc.cardemulation.AidGroup; 34 import android.nfc.cardemulation.ApduServiceInfo; 35 import android.nfc.cardemulation.CardEmulation; 36 import android.nfc.cardemulation.HostApduService; 37 import android.nfc.cardemulation.OffHostApduService; 38 import android.os.UserHandle; 39 import android.util.AtomicFile; 40 import android.util.Log; 41 import android.util.SparseArray; 42 import android.util.Xml; 43 44 import com.android.internal.util.FastXmlSerializer; 45 import com.google.android.collect.Maps; 46 47 import java.io.File; 48 import java.io.FileDescriptor; 49 import java.io.FileInputStream; 50 import java.io.FileOutputStream; 51 import java.io.IOException; 52 import java.io.PrintWriter; 53 import java.util.ArrayList; 54 import java.util.Collections; 55 import java.util.HashMap; 56 import java.util.Iterator; 57 import java.util.List; 58 import java.util.Map; 59 import java.util.concurrent.atomic.AtomicReference; 60 61 /** 62 * This class is inspired by android.content.pm.RegisteredServicesCache 63 * That class was not re-used because it doesn't support dynamically 64 * registering additional properties, but generates everything from 65 * the manifest. Since we have some properties that are not in the manifest, 66 * it's less suited. 67 */ 68 public class RegisteredServicesCache { 69 static final String XML_INDENT_OUTPUT_FEATURE = "http://xmlpull.org/v1/doc/features.html#indent-output"; 70 static final String TAG = "RegisteredServicesCache"; 71 static final boolean DEBUG = false; 72 73 final Context mContext; 74 final AtomicReference<BroadcastReceiver> mReceiver; 75 76 final Object mLock = new Object(); 77 // All variables below synchronized on mLock 78 79 // mUserServices holds the card emulation services that are running for each user 80 final SparseArray<UserServices> mUserServices = new SparseArray<UserServices>(); 81 final Callback mCallback; 82 final AtomicFile mDynamicAidsFile; 83 84 public interface Callback { onServicesUpdated(int userId, final List<ApduServiceInfo> services)85 void onServicesUpdated(int userId, final List<ApduServiceInfo> services); 86 }; 87 88 static class DynamicAids { 89 public final int uid; 90 public final HashMap<String, AidGroup> aidGroups = Maps.newHashMap(); 91 DynamicAids(int uid)92 DynamicAids(int uid) { 93 this.uid = uid; 94 } 95 }; 96 97 private static class UserServices { 98 /** 99 * All services that have registered 100 */ 101 final HashMap<ComponentName, ApduServiceInfo> services = 102 Maps.newHashMap(); // Re-built at run-time 103 final HashMap<ComponentName, DynamicAids> dynamicAids = 104 Maps.newHashMap(); // In memory cache of dynamic AID store 105 }; 106 findOrCreateUserLocked(int userId)107 private UserServices findOrCreateUserLocked(int userId) { 108 UserServices services = mUserServices.get(userId); 109 if (services == null) { 110 services = new UserServices(); 111 mUserServices.put(userId, services); 112 } 113 return services; 114 } 115 RegisteredServicesCache(Context context, Callback callback)116 public RegisteredServicesCache(Context context, Callback callback) { 117 mContext = context; 118 mCallback = callback; 119 120 final BroadcastReceiver receiver = new BroadcastReceiver() { 121 @Override 122 public void onReceive(Context context, Intent intent) { 123 final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); 124 String action = intent.getAction(); 125 if (DEBUG) Log.d(TAG, "Intent action: " + action); 126 if (uid != -1) { 127 boolean replaced = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false) && 128 (Intent.ACTION_PACKAGE_ADDED.equals(action) || 129 Intent.ACTION_PACKAGE_REMOVED.equals(action)); 130 if (!replaced) { 131 int currentUser = ActivityManager.getCurrentUser(); 132 if (currentUser == UserHandle.getUserId(uid)) { 133 invalidateCache(UserHandle.getUserId(uid)); 134 } else { 135 // Cache will automatically be updated on user switch 136 } 137 } else { 138 if (DEBUG) Log.d(TAG, "Ignoring package intent due to package being replaced."); 139 } 140 } 141 } 142 }; 143 mReceiver = new AtomicReference<BroadcastReceiver>(receiver); 144 145 IntentFilter intentFilter = new IntentFilter(); 146 intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 147 intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); 148 intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); 149 intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); 150 intentFilter.addAction(Intent.ACTION_PACKAGE_FIRST_LAUNCH); 151 intentFilter.addAction(Intent.ACTION_PACKAGE_RESTARTED); 152 intentFilter.addDataScheme("package"); 153 mContext.registerReceiverAsUser(mReceiver.get(), UserHandle.ALL, intentFilter, null, null); 154 155 // Register for events related to sdcard operations 156 IntentFilter sdFilter = new IntentFilter(); 157 sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); 158 sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); 159 mContext.registerReceiverAsUser(mReceiver.get(), UserHandle.ALL, sdFilter, null, null); 160 161 File dataDir = mContext.getFilesDir(); 162 mDynamicAidsFile = new AtomicFile(new File(dataDir, "dynamic_aids.xml")); 163 } 164 initialize()165 void initialize() { 166 synchronized (mLock) { 167 readDynamicAidsLocked(); 168 } 169 invalidateCache(ActivityManager.getCurrentUser()); 170 } 171 dump(ArrayList<ApduServiceInfo> services)172 void dump(ArrayList<ApduServiceInfo> services) { 173 for (ApduServiceInfo service : services) { 174 if (DEBUG) Log.d(TAG, service.toString()); 175 } 176 } 177 containsServiceLocked(ArrayList<ApduServiceInfo> services, ComponentName serviceName)178 boolean containsServiceLocked(ArrayList<ApduServiceInfo> services, ComponentName serviceName) { 179 for (ApduServiceInfo service : services) { 180 if (service.getComponent().equals(serviceName)) return true; 181 } 182 return false; 183 } 184 hasService(int userId, ComponentName service)185 public boolean hasService(int userId, ComponentName service) { 186 return getService(userId, service) != null; 187 } 188 getService(int userId, ComponentName service)189 public ApduServiceInfo getService(int userId, ComponentName service) { 190 synchronized (mLock) { 191 UserServices userServices = findOrCreateUserLocked(userId); 192 return userServices.services.get(service); 193 } 194 } 195 getServices(int userId)196 public List<ApduServiceInfo> getServices(int userId) { 197 final ArrayList<ApduServiceInfo> services = new ArrayList<ApduServiceInfo>(); 198 synchronized (mLock) { 199 UserServices userServices = findOrCreateUserLocked(userId); 200 services.addAll(userServices.services.values()); 201 } 202 return services; 203 } 204 getServicesForCategory(int userId, String category)205 public List<ApduServiceInfo> getServicesForCategory(int userId, String category) { 206 final ArrayList<ApduServiceInfo> services = new ArrayList<ApduServiceInfo>(); 207 synchronized (mLock) { 208 UserServices userServices = findOrCreateUserLocked(userId); 209 for (ApduServiceInfo service : userServices.services.values()) { 210 if (service.hasCategory(category)) services.add(service); 211 } 212 } 213 return services; 214 } 215 getInstalledServices(int userId)216 ArrayList<ApduServiceInfo> getInstalledServices(int userId) { 217 PackageManager pm; 218 try { 219 pm = mContext.createPackageContextAsUser("android", 0, 220 new UserHandle(userId)).getPackageManager(); 221 } catch (NameNotFoundException e) { 222 Log.e(TAG, "Could not create user package context"); 223 return null; 224 } 225 226 ArrayList<ApduServiceInfo> validServices = new ArrayList<ApduServiceInfo>(); 227 228 List<ResolveInfo> resolvedServices = new ArrayList<>(pm.queryIntentServicesAsUser( 229 new Intent(HostApduService.SERVICE_INTERFACE), 230 PackageManager.GET_META_DATA, userId)); 231 232 List<ResolveInfo> resolvedOffHostServices = pm.queryIntentServicesAsUser( 233 new Intent(OffHostApduService.SERVICE_INTERFACE), 234 PackageManager.GET_META_DATA, userId); 235 resolvedServices.addAll(resolvedOffHostServices); 236 237 for (ResolveInfo resolvedService : resolvedServices) { 238 try { 239 boolean onHost = !resolvedOffHostServices.contains(resolvedService); 240 ServiceInfo si = resolvedService.serviceInfo; 241 ComponentName componentName = new ComponentName(si.packageName, si.name); 242 // Check if the package holds the NFC permission 243 if (pm.checkPermission(android.Manifest.permission.NFC, si.packageName) != 244 PackageManager.PERMISSION_GRANTED) { 245 Log.e(TAG, "Skipping application component " + componentName + 246 ": it must request the permission " + 247 android.Manifest.permission.NFC); 248 continue; 249 } 250 if (!android.Manifest.permission.BIND_NFC_SERVICE.equals( 251 si.permission)) { 252 Log.e(TAG, "Skipping APDU service " + componentName + 253 ": it does not require the permission " + 254 android.Manifest.permission.BIND_NFC_SERVICE); 255 continue; 256 } 257 ApduServiceInfo service = new ApduServiceInfo(pm, resolvedService, onHost); 258 if (service != null) { 259 validServices.add(service); 260 } 261 } catch (XmlPullParserException e) { 262 Log.w(TAG, "Unable to load component info " + resolvedService.toString(), e); 263 } catch (IOException e) { 264 Log.w(TAG, "Unable to load component info " + resolvedService.toString(), e); 265 } 266 } 267 268 return validServices; 269 } 270 invalidateCache(int userId)271 public void invalidateCache(int userId) { 272 final ArrayList<ApduServiceInfo> validServices = getInstalledServices(userId); 273 if (validServices == null) { 274 return; 275 } 276 synchronized (mLock) { 277 UserServices userServices = findOrCreateUserLocked(userId); 278 279 // Find removed services 280 Iterator<Map.Entry<ComponentName, ApduServiceInfo>> it = 281 userServices.services.entrySet().iterator(); 282 while (it.hasNext()) { 283 Map.Entry<ComponentName, ApduServiceInfo> entry = 284 (Map.Entry<ComponentName, ApduServiceInfo>) it.next(); 285 if (!containsServiceLocked(validServices, entry.getKey())) { 286 Log.d(TAG, "Service removed: " + entry.getKey()); 287 it.remove(); 288 } 289 } 290 for (ApduServiceInfo service : validServices) { 291 if (DEBUG) Log.d(TAG, "Adding service: " + service.getComponent() + 292 " AIDs: " + service.getAids()); 293 userServices.services.put(service.getComponent(), service); 294 } 295 296 // Apply dynamic AID mappings 297 ArrayList<ComponentName> toBeRemoved = new ArrayList<ComponentName>(); 298 for (Map.Entry<ComponentName, DynamicAids> entry : 299 userServices.dynamicAids.entrySet()) { 300 // Verify component / uid match 301 ComponentName component = entry.getKey(); 302 DynamicAids dynamicAids = entry.getValue(); 303 ApduServiceInfo serviceInfo = userServices.services.get(component); 304 if (serviceInfo == null || (serviceInfo.getUid() != dynamicAids.uid)) { 305 toBeRemoved.add(component); 306 continue; 307 } else { 308 for (AidGroup group : dynamicAids.aidGroups.values()) { 309 serviceInfo.setOrReplaceDynamicAidGroup(group); 310 } 311 } 312 } 313 314 if (toBeRemoved.size() > 0) { 315 for (ComponentName component : toBeRemoved) { 316 Log.d(TAG, "Removing dynamic AIDs registered by " + component); 317 userServices.dynamicAids.remove(component); 318 } 319 // Persist to filesystem 320 writeDynamicAidsLocked(); 321 } 322 } 323 324 mCallback.onServicesUpdated(userId, Collections.unmodifiableList(validServices)); 325 dump(validServices); 326 } 327 readDynamicAidsLocked()328 private void readDynamicAidsLocked() { 329 FileInputStream fis = null; 330 try { 331 if (!mDynamicAidsFile.getBaseFile().exists()) { 332 Log.d(TAG, "Dynamic AIDs file does not exist."); 333 return; 334 } 335 fis = mDynamicAidsFile.openRead(); 336 XmlPullParser parser = Xml.newPullParser(); 337 parser.setInput(fis, null); 338 int eventType = parser.getEventType(); 339 while (eventType != XmlPullParser.START_TAG && 340 eventType != XmlPullParser.END_DOCUMENT) { 341 eventType = parser.next(); 342 } 343 String tagName = parser.getName(); 344 if ("services".equals(tagName)) { 345 boolean inService = false; 346 ComponentName currentComponent = null; 347 int currentUid = -1; 348 ArrayList<AidGroup> currentGroups = new ArrayList<AidGroup>(); 349 while (eventType != XmlPullParser.END_DOCUMENT) { 350 tagName = parser.getName(); 351 if (eventType == XmlPullParser.START_TAG) { 352 if ("service".equals(tagName) && parser.getDepth() == 2) { 353 String compString = parser.getAttributeValue(null, "component"); 354 String uidString = parser.getAttributeValue(null, "uid"); 355 if (compString == null || uidString == null) { 356 Log.e(TAG, "Invalid service attributes"); 357 } else { 358 try { 359 currentUid = Integer.parseInt(uidString); 360 currentComponent = ComponentName.unflattenFromString(compString); 361 inService = true; 362 } catch (NumberFormatException e) { 363 Log.e(TAG, "Could not parse service uid"); 364 } 365 } 366 } 367 if ("aid-group".equals(tagName) && parser.getDepth() == 3 && inService) { 368 AidGroup group = AidGroup.createFromXml(parser); 369 if (group != null) { 370 currentGroups.add(group); 371 } else { 372 Log.e(TAG, "Could not parse AID group."); 373 } 374 } 375 } else if (eventType == XmlPullParser.END_TAG) { 376 if ("service".equals(tagName)) { 377 // See if we have a valid service 378 if (currentComponent != null && currentUid >= 0 && 379 currentGroups.size() > 0) { 380 final int userId = UserHandle.getUserId(currentUid); 381 DynamicAids dynAids = new DynamicAids(currentUid); 382 for (AidGroup group : currentGroups) { 383 dynAids.aidGroups.put(group.getCategory(), group); 384 } 385 UserServices services = findOrCreateUserLocked(userId); 386 services.dynamicAids.put(currentComponent, dynAids); 387 } 388 currentUid = -1; 389 currentComponent = null; 390 currentGroups.clear(); 391 inService = false; 392 } 393 } 394 eventType = parser.next(); 395 }; 396 } 397 } catch (Exception e) { 398 Log.e(TAG, "Could not parse dynamic AIDs file, trashing."); 399 mDynamicAidsFile.delete(); 400 } finally { 401 if (fis != null) { 402 try { 403 fis.close(); 404 } catch (IOException e) { 405 } 406 } 407 } 408 } 409 writeDynamicAidsLocked()410 private boolean writeDynamicAidsLocked() { 411 FileOutputStream fos = null; 412 try { 413 fos = mDynamicAidsFile.startWrite(); 414 XmlSerializer out = new FastXmlSerializer(); 415 out.setOutput(fos, "utf-8"); 416 out.startDocument(null, true); 417 out.setFeature(XML_INDENT_OUTPUT_FEATURE, true); 418 out.startTag(null, "services"); 419 for (int i = 0; i < mUserServices.size(); i++) { 420 final UserServices user = mUserServices.valueAt(i); 421 for (Map.Entry<ComponentName, DynamicAids> service : user.dynamicAids.entrySet()) { 422 out.startTag(null, "service"); 423 out.attribute(null, "component", service.getKey().flattenToString()); 424 out.attribute(null, "uid", Integer.toString(service.getValue().uid)); 425 for (AidGroup group : service.getValue().aidGroups.values()) { 426 group.writeAsXml(out); 427 } 428 out.endTag(null, "service"); 429 } 430 } 431 out.endTag(null, "services"); 432 out.endDocument(); 433 mDynamicAidsFile.finishWrite(fos); 434 return true; 435 } catch (Exception e) { 436 Log.e(TAG, "Error writing dynamic AIDs", e); 437 if (fos != null) { 438 mDynamicAidsFile.failWrite(fos); 439 } 440 return false; 441 } 442 } 443 registerAidGroupForService(int userId, int uid, ComponentName componentName, AidGroup aidGroup)444 public boolean registerAidGroupForService(int userId, int uid, 445 ComponentName componentName, AidGroup aidGroup) { 446 ArrayList<ApduServiceInfo> newServices = null; 447 boolean success; 448 synchronized (mLock) { 449 UserServices services = findOrCreateUserLocked(userId); 450 // Check if we can find this service 451 ApduServiceInfo serviceInfo = getService(userId, componentName); 452 if (serviceInfo == null) { 453 Log.e(TAG, "Service " + componentName + " does not exist."); 454 return false; 455 } 456 if (serviceInfo.getUid() != uid) { 457 // This is probably a good indication something is wrong here. 458 // Either newer service installed with different uid (but then 459 // we should have known about it), or somebody calling us from 460 // a different uid. 461 Log.e(TAG, "UID mismatch."); 462 return false; 463 } 464 // Do another AID validation, since a caller could have thrown in a modified 465 // AidGroup object with invalid AIDs over Binder. 466 List<String> aids = aidGroup.getAids(); 467 for (String aid : aids) { 468 if (!CardEmulation.isValidAid(aid)) { 469 Log.e(TAG, "AID " + aid + " is not a valid AID"); 470 return false; 471 } 472 } 473 serviceInfo.setOrReplaceDynamicAidGroup(aidGroup); 474 DynamicAids dynAids = services.dynamicAids.get(componentName); 475 if (dynAids == null) { 476 dynAids = new DynamicAids(uid); 477 services.dynamicAids.put(componentName, dynAids); 478 } 479 dynAids.aidGroups.put(aidGroup.getCategory(), aidGroup); 480 success = writeDynamicAidsLocked(); 481 if (success) { 482 newServices = new ArrayList<ApduServiceInfo>(services.services.values()); 483 } else { 484 Log.e(TAG, "Failed to persist AID group."); 485 // Undo registration 486 dynAids.aidGroups.remove(aidGroup.getCategory()); 487 } 488 } 489 if (success) { 490 // Make callback without the lock held 491 mCallback.onServicesUpdated(userId, newServices); 492 } 493 return success; 494 } 495 getAidGroupForService(int userId, int uid, ComponentName componentName, String category)496 public AidGroup getAidGroupForService(int userId, int uid, ComponentName componentName, 497 String category) { 498 ApduServiceInfo serviceInfo = getService(userId, componentName); 499 if (serviceInfo != null) { 500 if (serviceInfo.getUid() != uid) { 501 Log.e(TAG, "UID mismatch"); 502 return null; 503 } 504 return serviceInfo.getDynamicAidGroupForCategory(category); 505 } else { 506 Log.e(TAG, "Could not find service " + componentName); 507 return null; 508 } 509 } 510 removeAidGroupForService(int userId, int uid, ComponentName componentName, String category)511 public boolean removeAidGroupForService(int userId, int uid, ComponentName componentName, 512 String category) { 513 boolean success = false; 514 ArrayList<ApduServiceInfo> newServices = null; 515 synchronized (mLock) { 516 UserServices services = findOrCreateUserLocked(userId); 517 ApduServiceInfo serviceInfo = getService(userId, componentName); 518 if (serviceInfo != null) { 519 if (serviceInfo.getUid() != uid) { 520 // Calling from different uid 521 Log.e(TAG, "UID mismatch"); 522 return false; 523 } 524 if (!serviceInfo.removeDynamicAidGroupForCategory(category)) { 525 Log.e(TAG," Could not find dynamic AIDs for category " + category); 526 return false; 527 } 528 // Remove from local cache 529 DynamicAids dynAids = services.dynamicAids.get(componentName); 530 if (dynAids != null) { 531 AidGroup deletedGroup = dynAids.aidGroups.remove(category); 532 success = writeDynamicAidsLocked(); 533 if (success) { 534 newServices = new ArrayList<ApduServiceInfo>(services.services.values()); 535 } else { 536 Log.e(TAG, "Could not persist deleted AID group."); 537 dynAids.aidGroups.put(category, deletedGroup); 538 return false; 539 } 540 } else { 541 Log.e(TAG, "Could not find aid group in local cache."); 542 } 543 } else { 544 Log.e(TAG, "Service " + componentName + " does not exist."); 545 } 546 } 547 if (success) { 548 mCallback.onServicesUpdated(userId, newServices); 549 } 550 return success; 551 } 552 dump(FileDescriptor fd, PrintWriter pw, String[] args)553 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 554 pw.println("Registered HCE services for current user: "); 555 UserServices userServices = findOrCreateUserLocked(ActivityManager.getCurrentUser()); 556 for (ApduServiceInfo service : userServices.services.values()) { 557 service.dump(fd, pw, args); 558 pw.println(""); 559 } 560 pw.println(""); 561 } 562 563 } 564