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