/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.nfc.cardemulation; import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.nfc.cardemulation.ApduServiceInfo; import android.nfc.cardemulation.CardEmulation; import android.util.Log; import com.google.android.collect.Maps; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.NavigableMap; import java.util.PriorityQueue; import java.util.TreeMap; public class RegisteredAidCache { static final String TAG = "RegisteredAidCache"; static final boolean DBG = false; // mAidServices maps AIDs to services that have registered them. // It's a TreeMap in order to be able to quickly select subsets // of AIDs that conflict with each other. final TreeMap> mAidServices = new TreeMap>(); // mAidCache is a lookup table for quickly mapping an exact or prefix AID to one or // more handling services. It differs from mAidServices in the sense that it // has already accounted for defaults, and hence its return value // is authoritative for the current set of services and defaults. // It is only valid for the current user. final TreeMap mAidCache = new TreeMap(); // Represents a single AID registration of a service final class ServiceAidInfo { ApduServiceInfo service; String aid; String category; @Override public String toString() { return "ServiceAidInfo{" + "service=" + service.getComponent() + ", aid='" + aid + '\'' + ", category='" + category + '\'' + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ServiceAidInfo that = (ServiceAidInfo) o; if (!aid.equals(that.aid)) return false; if (!category.equals(that.category)) return false; if (!service.equals(that.service)) return false; return true; } @Override public int hashCode() { int result = service.hashCode(); result = 31 * result + aid.hashCode(); result = 31 * result + category.hashCode(); return result; } } // Represents a list of services, an optional default and a category that // an AID was resolved to. final class AidResolveInfo { List services = new ArrayList(); ApduServiceInfo defaultService = null; String category = null; boolean mustRoute = true; // Whether this AID should be routed at all @Override public String toString() { return "AidResolveInfo{" + "services=" + services + ", defaultService=" + defaultService + ", category='" + category + '\'' + ", mustRoute=" + mustRoute + '}'; } } final AidResolveInfo EMPTY_RESOLVE_INFO = new AidResolveInfo(); final Context mContext; final AidRoutingManager mRoutingManager; final Object mLock = new Object(); ComponentName mPreferredPaymentService; ComponentName mPreferredForegroundService; boolean mNfcEnabled = false; boolean mSupportsPrefixes = false; public RegisteredAidCache(Context context) { mContext = context; mRoutingManager = new AidRoutingManager(); mPreferredPaymentService = null; mPreferredForegroundService = null; mSupportsPrefixes = mRoutingManager.supportsAidPrefixRouting(); if (mSupportsPrefixes) { if (DBG) Log.d(TAG, "Controller supports AID prefix routing"); } } public AidResolveInfo resolveAid(String aid) { synchronized (mLock) { if (DBG) Log.d(TAG, "resolveAid: resolving AID " + aid); if (aid.length() < 10) { Log.e(TAG, "AID selected with fewer than 5 bytes."); return EMPTY_RESOLVE_INFO; } AidResolveInfo resolveInfo = new AidResolveInfo(); if (mSupportsPrefixes) { // Our AID cache may contain prefixes which also match this AID, // so we must find all potential prefixes and merge the ResolveInfo // of those prefixes plus any exact match in a single result. String shortestAidMatch = aid.substring(0, 10); // Minimum AID length is 5 bytes String longestAidMatch = aid + "*"; // Longest potential matching AID if (DBG) Log.d(TAG, "Finding AID registrations in range [" + shortestAidMatch + " - " + longestAidMatch + "]"); NavigableMap matchingAids = mAidCache.subMap(shortestAidMatch, true, longestAidMatch, true); resolveInfo.category = CardEmulation.CATEGORY_OTHER; for (Map.Entry entry : matchingAids.entrySet()) { boolean isPrefix = isPrefix(entry.getKey()); String entryAid = isPrefix ? entry.getKey().substring(0, entry.getKey().length() - 1) : entry.getKey(); // Cut off '*' if prefix if (entryAid.equalsIgnoreCase(aid) || (isPrefix && aid.startsWith(entryAid))) { if (DBG) Log.d(TAG, "resolveAid: AID " + entry.getKey() + " matches."); AidResolveInfo entryResolveInfo = entry.getValue(); if (entryResolveInfo.defaultService != null) { if (resolveInfo.defaultService != null) { // This shouldn't happen; for every prefix we have only one // default service. Log.e(TAG, "Different defaults for conflicting AIDs!"); } resolveInfo.defaultService = entryResolveInfo.defaultService; resolveInfo.category = entryResolveInfo.category; } for (ApduServiceInfo serviceInfo : entryResolveInfo.services) { if (!resolveInfo.services.contains(serviceInfo)) { resolveInfo.services.add(serviceInfo); } } } } } else { resolveInfo = mAidCache.get(aid); } if (DBG) Log.d(TAG, "Resolved to: " + resolveInfo); return resolveInfo; } } public boolean supportsAidPrefixRegistration() { return mSupportsPrefixes; } public boolean isDefaultServiceForAid(int userId, ComponentName service, String aid) { AidResolveInfo resolveInfo = resolveAid(aid); if (resolveInfo == null || resolveInfo.services == null || resolveInfo.services.size() == 0) { return false; } if (resolveInfo.defaultService != null) { return service.equals(resolveInfo.defaultService.getComponent()); } else if (resolveInfo.services.size() == 1) { return service.equals(resolveInfo.services.get(0).getComponent()); } else { // More than one service, not the default return false; } } /** * Resolves a conflict between multiple services handling the same * AIDs. Note that the AID itself is not an input to the decision * process - the algorithm just looks at the competing services * and what preferences the user has indicated. In short, it works like * this: * * 1) If there is a preferred foreground service, that service wins * 2) Else, if there is a preferred payment service, that service wins * 3) Else, if there is no winner, and all conflicting services will be * in the list of resolved services. */ AidResolveInfo resolveAidConflictLocked(Collection conflictingServices, boolean makeSingleServiceDefault) { if (conflictingServices == null || conflictingServices.size() == 0) { Log.e(TAG, "resolveAidConflict: No services passed in."); return null; } AidResolveInfo resolveInfo = new AidResolveInfo(); resolveInfo.category = CardEmulation.CATEGORY_OTHER; ApduServiceInfo matchedForeground = null; ApduServiceInfo matchedPayment = null; for (ServiceAidInfo serviceAidInfo : conflictingServices) { boolean serviceClaimsPaymentAid = CardEmulation.CATEGORY_PAYMENT.equals(serviceAidInfo.category); if (serviceAidInfo.service.getComponent().equals(mPreferredForegroundService)) { resolveInfo.services.add(serviceAidInfo.service); if (serviceClaimsPaymentAid) { resolveInfo.category = CardEmulation.CATEGORY_PAYMENT; } matchedForeground = serviceAidInfo.service; } else if (serviceAidInfo.service.getComponent().equals(mPreferredPaymentService) && serviceClaimsPaymentAid) { resolveInfo.services.add(serviceAidInfo.service); resolveInfo.category = CardEmulation.CATEGORY_PAYMENT; matchedPayment = serviceAidInfo.service; } else { if (serviceClaimsPaymentAid) { // If this service claims it's a payment AID, don't route it, // because it's not the default. Otherwise, add it to the list // but not as default. if (DBG) Log.d(TAG, "resolveAidLocked: (Ignoring handling service " + serviceAidInfo.service.getComponent() + " because it's not the payment default.)"); } else { resolveInfo.services.add(serviceAidInfo.service); } } } if (matchedForeground != null) { // 1st priority: if the foreground app prefers a service, // and that service asks for the AID, that service gets it if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to foreground preferred " + matchedForeground); resolveInfo.defaultService = matchedForeground; } else if (matchedPayment != null) { // 2nd priority: if there is a preferred payment service, // and that service claims this as a payment AID, that service gets it if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to payment default " + "default " + matchedPayment); resolveInfo.defaultService = matchedPayment; } else { if (resolveInfo.services.size() == 1 && makeSingleServiceDefault) { if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: making single handling service " + resolveInfo.services.get(0).getComponent() + " default."); resolveInfo.defaultService = resolveInfo.services.get(0); } else { // Nothing to do, all services already in list if (DBG) Log.d(TAG, "resolveAidLocked: DECISION: routing to all matching services"); } } return resolveInfo; } class DefaultServiceInfo { ServiceAidInfo paymentDefault; ServiceAidInfo foregroundDefault; } DefaultServiceInfo findDefaultServices(ArrayList serviceAidInfos) { DefaultServiceInfo defaultServiceInfo = new DefaultServiceInfo(); for (ServiceAidInfo serviceAidInfo : serviceAidInfos) { boolean serviceClaimsPaymentAid = CardEmulation.CATEGORY_PAYMENT.equals(serviceAidInfo.category); if (serviceAidInfo.service.getComponent().equals(mPreferredForegroundService)) { defaultServiceInfo.foregroundDefault = serviceAidInfo; } else if (serviceAidInfo.service.getComponent().equals(mPreferredPaymentService) && serviceClaimsPaymentAid) { defaultServiceInfo.paymentDefault = serviceAidInfo; } } return defaultServiceInfo; } AidResolveInfo resolvePrefixAidConflictLocked(ArrayList prefixServices, ArrayList conflictingServices) { // Find defaults among the prefix services themselves DefaultServiceInfo prefixDefaultInfo = findDefaultServices(prefixServices); // Find any defaults among the children DefaultServiceInfo conflictingDefaultInfo = findDefaultServices(conflictingServices); // Three conditions under which the prefix root AID gets to be the default // 1. A service registering the prefix root AID is the current foreground preferred // 2. A service registering the prefix root AID is the current tap & pay default AND // no child is the current foreground preferred // 3. There is only one service for the prefix root AID, and there are no children if (prefixDefaultInfo.foregroundDefault != null) { if (DBG) Log.d(TAG, "Prefix AID service " + prefixDefaultInfo.foregroundDefault.service.getComponent() + " has foreground" + " preference, ignoring conflicting AIDs."); // Foreground default trumps any conflicting services, treat as normal AID conflict // and ignore children return resolveAidConflictLocked(prefixServices, true); } else if (prefixDefaultInfo.paymentDefault != null) { // Check if any of the conflicting services is foreground default if (conflictingDefaultInfo.foregroundDefault != null) { // Conflicting AID registration is in foreground, trumps prefix tap&pay default if (DBG) Log.d(TAG, "One of the conflicting AID registrations is foreground " + "preferred, ignoring prefix."); return EMPTY_RESOLVE_INFO; } else { // Prefix service is tap&pay default, treat as normal AID conflict for just prefix if (DBG) Log.d(TAG, "Prefix AID service " + prefixDefaultInfo.paymentDefault.service.getComponent() + " is payment" + " default, ignoring conflicting AIDs."); return resolveAidConflictLocked(prefixServices, true); } } else { if (conflictingDefaultInfo.foregroundDefault != null || conflictingDefaultInfo.paymentDefault != null) { if (DBG) Log.d(TAG, "One of the conflicting AID registrations is either payment " + "default or foreground preferred, ignoring prefix."); return EMPTY_RESOLVE_INFO; } else { // No children that are preferred; add all services of the root // make single service default if no children are present if (DBG) Log.d(TAG, "No service has preference, adding all."); return resolveAidConflictLocked(prefixServices, conflictingServices.isEmpty()); } } } void generateServiceMapLocked(List services) { // Easiest is to just build the entire tree again mAidServices.clear(); for (ApduServiceInfo service : services) { if (DBG) Log.d(TAG, "generateServiceMap component: " + service.getComponent()); List prefixAids = service.getPrefixAids(); for (String aid : service.getAids()) { if (!CardEmulation.isValidAid(aid)) { Log.e(TAG, "Aid " + aid + " is not valid."); continue; } if (aid.endsWith("*") && !supportsAidPrefixRegistration()) { Log.e(TAG, "Prefix AID " + aid + " ignored on device that doesn't support it."); continue; } else if (supportsAidPrefixRegistration() && prefixAids.size() > 0 && !isPrefix(aid)) { // Check if we already have an overlapping prefix registered for this AID boolean foundPrefix = false; for (String prefixAid : prefixAids) { String prefix = prefixAid.substring(0, prefixAid.length() - 1); if (aid.startsWith(prefix)) { Log.e(TAG, "Ignoring exact AID " + aid + " because prefix AID " + prefixAid + " is already registered"); foundPrefix = true; break; } } if (foundPrefix) { continue; } } ServiceAidInfo serviceAidInfo = new ServiceAidInfo(); serviceAidInfo.aid = aid.toUpperCase(); serviceAidInfo.service = service; serviceAidInfo.category = service.getCategoryForAid(aid); if (mAidServices.containsKey(serviceAidInfo.aid)) { final ArrayList serviceAidInfos = mAidServices.get(serviceAidInfo.aid); serviceAidInfos.add(serviceAidInfo); } else { final ArrayList serviceAidInfos = new ArrayList(); serviceAidInfos.add(serviceAidInfo); mAidServices.put(serviceAidInfo.aid, serviceAidInfos); } } } } static boolean isPrefix(String aid) { return aid.endsWith("*"); } final class PrefixConflicts { NavigableMap> conflictMap; final ArrayList services = new ArrayList(); final HashSet aids = new HashSet(); } PrefixConflicts findConflictsForPrefixLocked(String prefixAid) { PrefixConflicts prefixConflicts = new PrefixConflicts(); String plainAid = prefixAid.substring(0, prefixAid.length() - 1); // Cut off "*" String lastAidWithPrefix = String.format("%-32s", plainAid).replace(' ', 'F'); if (DBG) Log.d(TAG, "Finding AIDs in range [" + plainAid + " - " + lastAidWithPrefix + "]"); prefixConflicts.conflictMap = mAidServices.subMap(plainAid, true, lastAidWithPrefix, true); for (Map.Entry> entry : prefixConflicts.conflictMap.entrySet()) { if (!entry.getKey().equalsIgnoreCase(prefixAid)) { if (DBG) Log.d(TAG, "AID " + entry.getKey() + " conflicts with prefix; " + " adding handling services for conflict resolution."); prefixConflicts.services.addAll(entry.getValue()); prefixConflicts.aids.add(entry.getKey()); } } return prefixConflicts; } void generateAidCacheLocked() { mAidCache.clear(); // Get all exact and prefix AIDs in an ordered list PriorityQueue aidsToResolve = new PriorityQueue(mAidServices.keySet()); while (!aidsToResolve.isEmpty()) { final ArrayList resolvedAids = new ArrayList(); String aidToResolve = aidsToResolve.peek(); // Because of the lexicographical ordering, all following AIDs either start with the // same bytes and are longer, or start with different bytes. // A special case is if another service registered the same AID as a prefix, in // which case we want to start with that AID, since it conflicts with this one if (aidsToResolve.contains(aidToResolve + "*")) { aidToResolve = aidToResolve + "*"; } if (DBG) Log.d(TAG, "generateAidCacheLocked: starting with aid " + aidToResolve); if (isPrefix(aidToResolve)) { // This AID itself is a prefix; let's consider this prefix as the "root", // and all conflicting AIDs as its children. // For example, if "A000000003*" is the prefix root, // "A000000003", "A00000000301*", "A0000000030102" are all conflicting children AIDs final ArrayList prefixServices = new ArrayList( mAidServices.get(aidToResolve)); // Find all conflicting children services PrefixConflicts prefixConflicts = findConflictsForPrefixLocked(aidToResolve); // Resolve conflicts AidResolveInfo resolveInfo = resolvePrefixAidConflictLocked(prefixServices, prefixConflicts.services); mAidCache.put(aidToResolve, resolveInfo); resolvedAids.add(aidToResolve); if (resolveInfo.defaultService != null) { // This prefix is the default; therefore, AIDs of all conflicting children // will no longer be evaluated. resolvedAids.addAll(prefixConflicts.aids); } else if (resolveInfo.services.size() > 0) { // This means we don't have a default for this prefix and all its // conflicting children. So, for all conflicting AIDs, just add // all handling services without setting a default boolean foundChildService = false; for (Map.Entry> entry : prefixConflicts.conflictMap.entrySet()) { if (!entry.getKey().equalsIgnoreCase(aidToResolve)) { if (DBG) Log.d(TAG, "AID " + entry.getKey() + " shared with prefix; " + " adding all handling services."); AidResolveInfo childResolveInfo = resolveAidConflictLocked( entry.getValue(), false); // Special case: in this case all children AIDs must be routed to the // host, so we can ask the user which service is preferred. // Since these are all "children" of the prefix, they don't need // to be routed, since the prefix will already get routed to the host childResolveInfo.mustRoute = false; mAidCache.put(entry.getKey(),childResolveInfo); resolvedAids.add(entry.getKey()); foundChildService |= !childResolveInfo.services.isEmpty(); } } // Special case: if in the end we didn't add any children services, // and the prefix has only one service, make that default if (!foundChildService && resolveInfo.services.size() == 1) { resolveInfo.defaultService = resolveInfo.services.get(0); } } else { // This prefix is not handled at all; we will evaluate // the children separately in next passes. } } else { // Exact AID and no other conflicting AID registrations present // This is true because aidsToResolve is lexicographically ordered, and // so by necessity all other AIDs are different than this AID or longer. if (DBG) Log.d(TAG, "Exact AID, resolving."); final ArrayList conflictingServiceInfos = new ArrayList(mAidServices.get(aidToResolve)); mAidCache.put(aidToResolve, resolveAidConflictLocked(conflictingServiceInfos, true)); resolvedAids.add(aidToResolve); } // Remove the AIDs we resolved from the list of AIDs to resolve if (DBG) Log.d(TAG, "AIDs: " + resolvedAids + " were resolved."); aidsToResolve.removeAll(resolvedAids); resolvedAids.clear(); } updateRoutingLocked(); } void updateRoutingLocked() { if (!mNfcEnabled) { if (DBG) Log.d(TAG, "Not updating routing table because NFC is off."); return; } final HashMap routingEntries = Maps.newHashMap(); // For each AID, find interested services for (Map.Entry aidEntry: mAidCache.entrySet()) { String aid = aidEntry.getKey(); AidResolveInfo resolveInfo = aidEntry.getValue(); if (!resolveInfo.mustRoute) { if (DBG) Log.d(TAG, "Not routing AID " + aid + " on request."); continue; } if (resolveInfo.services.size() == 0) { // No interested services } else if (resolveInfo.defaultService != null) { // There is a default service set, route to where that service resides - // either on the host (HCE) or on an SE. routingEntries.put(aid, resolveInfo.defaultService.isOnHost()); } else if (resolveInfo.services.size() == 1) { // Only one service, but not the default, must route to host // to ask the user to choose one. routingEntries.put(aid, true); } else if (resolveInfo.services.size() > 1) { // Multiple services, need to route to host to ask routingEntries.put(aid, true); } } mRoutingManager.configureRouting(routingEntries); } public void onServicesUpdated(int userId, List services) { if (DBG) Log.d(TAG, "onServicesUpdated"); synchronized (mLock) { if (ActivityManager.getCurrentUser() == userId) { // Rebuild our internal data-structures generateServiceMapLocked(services); generateAidCacheLocked(); } else { if (DBG) Log.d(TAG, "Ignoring update because it's not for the current user."); } } } public void onPreferredPaymentServiceChanged(ComponentName service) { if (DBG) Log.d(TAG, "Preferred payment service changed."); synchronized (mLock) { mPreferredPaymentService = service; generateAidCacheLocked(); } } public void onPreferredForegroundServiceChanged(ComponentName service) { if (DBG) Log.d(TAG, "Preferred foreground service changed."); synchronized (mLock) { mPreferredForegroundService = service; generateAidCacheLocked(); } } public void onNfcDisabled() { synchronized (mLock) { mNfcEnabled = false; } mRoutingManager.onNfccRoutingTableCleared(); } public void onNfcEnabled() { synchronized (mLock) { mNfcEnabled = true; updateRoutingLocked(); } } String dumpEntry(Map.Entry entry) { StringBuilder sb = new StringBuilder(); String category = entry.getValue().category; ApduServiceInfo defaultServiceInfo = entry.getValue().defaultService; sb.append(" \"" + entry.getKey() + "\" (category: " + category + ")\n"); ComponentName defaultComponent = defaultServiceInfo != null ? defaultServiceInfo.getComponent() : null; for (ApduServiceInfo serviceInfo : entry.getValue().services) { sb.append(" "); if (serviceInfo.getComponent().equals(defaultComponent)) { sb.append("*DEFAULT* "); } sb.append(serviceInfo.getComponent() + " (Description: " + serviceInfo.getDescription() + ")\n"); } return sb.toString(); } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println(" AID cache entries: "); for (Map.Entry entry : mAidCache.entrySet()) { pw.println(dumpEntry(entry)); } pw.println(" Service preferred by foreground app: " + mPreferredForegroundService); pw.println(" Preferred payment service: " + mPreferredPaymentService); pw.println(""); mRoutingManager.dump(fd, pw, args); pw.println(""); } }