From c05387ef4e6d6d024b03254e7e2ea5ad001cf2d2 Mon Sep 17 00:00:00 2001 From: Jeff Hamilton Date: Tue, 25 Oct 2011 15:25:42 -0500 Subject: Implement static white-list access control for NFC-EE. NFC service now has a build time array of package signatures and package names that are allowed access to the NFCEE, removing the shared certificate requirement. See sample_nfcee_access.xml for the XML format. Bug: 4515759 Change-Id: Ibf03e259137f2f4247ec5d31fb88b1090b013d32 --- src/com/android/nfc/NfcService.java | 85 +++++--- src/com/android/nfc/NfceeAccessControl.java | 289 ++++++++++++++++++++++++++++ 2 files changed, 347 insertions(+), 27 deletions(-) create mode 100644 src/com/android/nfc/NfceeAccessControl.java (limited to 'src/com') diff --git a/src/com/android/nfc/NfcService.java b/src/com/android/nfc/NfcService.java index 337930e..58a5732 100755 --- a/src/com/android/nfc/NfcService.java +++ b/src/com/android/nfc/NfcService.java @@ -35,7 +35,9 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.media.AudioManager; import android.media.SoundPool; import android.net.Uri; @@ -71,22 +73,24 @@ import java.io.PrintWriter; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.concurrent.ExecutionException; public class NfcService extends Application implements DeviceHostListener { private static final String ACTION_MASTER_CLEAR_NOTIFICATION = "android.intent.action.MASTER_CLEAR_NOTIFICATION"; - static final boolean DBG = true; + static final boolean DBG = false; static final String TAG = "NfcService"; public static final String SERVICE_NAME = "nfc"; + /** Regular NFC permission */ private static final String NFC_PERM = android.Manifest.permission.NFC; private static final String NFC_PERM_ERROR = "NFC permission required"; + + /** NFC ADMIN permission - only for system apps */ private static final String ADMIN_PERM = android.Manifest.permission.WRITE_SECURE_SETTINGS; private static final String ADMIN_PERM_ERROR = "WRITE_SECURE_SETTINGS permission required"; - private static final String NFCEE_ADMIN_PERM = "com.android.nfc.permission.NFCEE_ADMIN"; - private static final String NFCEE_ADMIN_PERM_ERROR = "NFCEE_ADMIN permission required"; public static final String PREF = "NfcServicePrefs"; @@ -189,6 +193,7 @@ public class NfcService extends Application implements DeviceHostListener { NfcAdapterExtrasService mExtrasService; boolean mIsAirplaneSensitive; boolean mIsAirplaneToggleable; + NfceeAccessControl mNfceeAccessControl; private NfcDispatcher mNfcDispatcher; private KeyguardManager mKeyguard; @@ -196,16 +201,18 @@ public class NfcService extends Application implements DeviceHostListener { private static NfcService sService; public static void enforceAdminPerm(Context context) { - int admin = context.checkCallingOrSelfPermission(ADMIN_PERM); - int nfcee = context.checkCallingOrSelfPermission(NFCEE_ADMIN_PERM); - if (admin != PackageManager.PERMISSION_GRANTED - && nfcee != PackageManager.PERMISSION_GRANTED) { - throw new SecurityException(ADMIN_PERM_ERROR); - } + context.enforceCallingOrSelfPermission(ADMIN_PERM, ADMIN_PERM_ERROR); } - public static void enforceNfceeAdminPerm(Context context) { - context.enforceCallingOrSelfPermission(NFCEE_ADMIN_PERM, NFCEE_ADMIN_PERM_ERROR); + public void enforceNfceeAdminPerm(String pkg) { + if (pkg == null) { + throw new SecurityException("caller must pass a package name"); + } + mContext.enforceCallingOrSelfPermission(NFC_PERM, NFC_PERM_ERROR); + if (!mNfceeAccessControl.check(Binder.getCallingUid(), pkg)) { + throw new SecurityException(NfceeAccessControl.NFCEE_ACCESS_PATH + + " denies NFCEE access to " + pkg); + } } public static NfcService getInstance() { @@ -300,6 +307,8 @@ public class NfcService extends Application implements DeviceHostListener { mSecureElement = new NativeNfcSecureElement(); mEeRoutingState = ROUTE_OFF; + mNfceeAccessControl = new NfceeAccessControl(this); + mPrefs = getSharedPreferences(PREF, Context.MODE_PRIVATE); mPrefsEditor = mPrefs.edit(); @@ -687,8 +696,8 @@ public class NfcService extends Application implements DeviceHostListener { } @Override - public INfcAdapterExtras getNfcAdapterExtrasInterface() { - NfcService.enforceNfceeAdminPerm(mContext); + public INfcAdapterExtras getNfcAdapterExtrasInterface(String pkg) { + NfcService.this.enforceNfceeAdminPerm(pkg); return mExtrasService; } @@ -1112,8 +1121,8 @@ public class NfcService extends Application implements DeviceHostListener { } @Override - public Bundle open(IBinder b) throws RemoteException { - NfcService.enforceNfceeAdminPerm(mContext); + public Bundle open(String pkg, IBinder b) throws RemoteException { + NfcService.this.enforceNfceeAdminPerm(pkg); Bundle result; try { @@ -1156,8 +1165,10 @@ public class NfcService extends Application implements DeviceHostListener { } @Override - public Bundle close() throws RemoteException { - NfcService.enforceNfceeAdminPerm(mContext); + public Bundle close(String pkg, IBinder b) throws RemoteException { + NfcService.this.enforceNfceeAdminPerm(pkg); + + b.unlinkToDeath(mOpenEe, 0); Bundle result; try { @@ -1170,8 +1181,8 @@ public class NfcService extends Application implements DeviceHostListener { } @Override - public Bundle transceive(byte[] in) throws RemoteException { - NfcService.enforceNfceeAdminPerm(mContext); + public Bundle transceive(String pkg, byte[] in) throws RemoteException { + NfcService.this.enforceNfceeAdminPerm(pkg); Bundle result; byte[] out; @@ -1202,21 +1213,21 @@ public class NfcService extends Application implements DeviceHostListener { } @Override - public int getCardEmulationRoute() throws RemoteException { - NfcService.enforceNfceeAdminPerm(mContext); + public int getCardEmulationRoute(String pkg) throws RemoteException { + NfcService.this.enforceNfceeAdminPerm(pkg); return mEeRoutingState; } @Override - public void setCardEmulationRoute(int route) throws RemoteException { - NfcService.enforceNfceeAdminPerm(mContext); + public void setCardEmulationRoute(String pkg, int route) throws RemoteException { + NfcService.this.enforceNfceeAdminPerm(pkg); mEeRoutingState = route; applyRouting(); } @Override - public void authenticate(byte[] token) throws RemoteException { - NfcService.enforceNfceeAdminPerm(mContext); + public void authenticate(String pkg, byte[] token) throws RemoteException { + NfcService.this.enforceNfceeAdminPerm(pkg); } }; @@ -1527,9 +1538,24 @@ public class NfcService extends Application implements DeviceHostListener { } private void sendSeBroadcast(Intent intent) { - mNfcDispatcher.resumeAppSwitches(); + PackageManager pm = getPackageManager(); intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); - mContext.sendBroadcast(intent, NFCEE_ADMIN_PERM); + + // Resume app switches so the receivers can start activites without delay + mNfcDispatcher.resumeAppSwitches(); + + // Find the matching receivers and check each one for access permissions + List receivers = pm.queryBroadcastReceivers(intent, 0); + for (ResolveInfo receiver : receivers) { + ActivityInfo activityInfo = receiver.activityInfo; + if (activityInfo != null) { + if (mNfceeAccessControl.check(activityInfo.applicationInfo)) { + intent.setComponent(new ComponentName(receiver.activityInfo.packageName, + receiver.activityInfo.name)); + mContext.sendBroadcast(intent); + } + } + } } private boolean llcpActivated(NfcDepEndpoint device) { @@ -1692,6 +1718,9 @@ public class NfcService extends Application implements DeviceHostListener { Log.w(TAG, "failed to wipe NFC-EE"); } } else if (action.equals(Intent.ACTION_PACKAGE_REMOVED)) { + // Clear the NFCEE access cache in case a UID gets recycled + mNfceeAccessControl.invalidateCache(); + boolean dataRemoved = intent.getBooleanExtra(Intent.EXTRA_DATA_REMOVED, false); if (dataRemoved) { Uri data = intent.getData(); @@ -1762,7 +1791,9 @@ public class NfcService extends Application implements DeviceHostListener { pw.println("mIsAirplaneSensitive=" + mIsAirplaneSensitive); pw.println("mIsAirplaneToggleable=" + mIsAirplaneToggleable); mP2pLinkManager.dump(fd, pw, args); + mNfceeAccessControl.dump(fd, pw, args); pw.println(mDeviceHost.dump()); + } } } diff --git a/src/com/android/nfc/NfceeAccessControl.java b/src/com/android/nfc/NfceeAccessControl.java new file mode 100644 index 0000000..9056b99 --- /dev/null +++ b/src/com/android/nfc/NfceeAccessControl.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2011 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; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.Signature; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Environment; +import android.util.Log; + +public class NfceeAccessControl { + static final String TAG = "NfceeAccess"; + static final boolean DBG = true; + + public static final String NFCEE_ACCESS_PATH = "/etc/nfcee_access.xml"; + + /** + * Map of signatures to valid packages names, as read from nfcee_access.xml. + * An empty list of package names indicates that any package + * with this signature is allowed. + */ + final HashMap mNfceeAccess; // contents final after onCreate() + + /** + * Map from UID to NFCEE access, used as a cache. + * Note: if a UID contains multiple packages they must all be + * signed with the same certificate so in effect UID == certificate + * used to sign the package. + */ + final HashMap mUidCache; // contents guarded by this + + final Context mContext; + final boolean mDebugPrintSignature; + + NfceeAccessControl(Context context) { + mContext = context; + mNfceeAccess = new HashMap(); + mUidCache = new HashMap(); + mDebugPrintSignature = parseNfceeAccess(); + } + + /** + * Check if the {uid, pkg} combination may use NFCEE. + * Also verify with package manager that this {uid, pkg} combination + * is valid if it is not cached. + */ + public boolean check(int uid, String pkg) { + synchronized (this) { + Boolean cached = mUidCache.get(uid); + if (cached != null) { + return cached; + } + + boolean access = false; + + // Ensure the claimed package is present in the calling UID + PackageManager pm = mContext.getPackageManager(); + String[] pkgs = pm.getPackagesForUid(uid); + for (String uidPkg : pkgs) { + if (uidPkg.equals(pkg)) { + // Ensure the package has access permissions + if (checkPackageNfceeAccess(pkg)) { + access = true; + } + break; + } + } + + mUidCache.put(uid, access); + return access; + } + } + + /** + * Check if the given ApplicationInfo may use the NFCEE. + * Assumes ApplicationInfo came from package manager, + * so no need to confirm {uid, pkg} is valid. + */ + public boolean check(ApplicationInfo info) { + synchronized (this) { + Boolean access = mUidCache.get(info.uid); + if (access == null) { + access = checkPackageNfceeAccess(info.packageName); + mUidCache.put(info.uid, access); + } + return access; + } + } + + public void invalidateCache() { + synchronized (this) { + mUidCache.clear(); + } + } + + /** + * Check with package manager if the pkg may use NFCEE. + * Does not use cache. + */ + boolean checkPackageNfceeAccess(String pkg) { + PackageManager pm = mContext.getPackageManager(); + try { + PackageInfo info = pm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES); + if (info.signatures == null) { + return false; + } + + for (Signature s : info.signatures){ + if (s == null) { + continue; + } + String[] packages = mNfceeAccess.get(s); + if (packages == null) { + continue; + } + if (packages.length == 0) { + // wildcard access + if (DBG) Log.d(TAG, "Granted NFCEE access to " + pkg + " (wildcard)"); + return true; + } + for (String p : packages) { + if (pkg.equals(p)) { + // explicit package access + if (DBG) Log.d(TAG, "Granted access to " + pkg + " (explicit)"); + return true; + } + } + } + + if (mDebugPrintSignature) { + Log.w(TAG, "denied NFCEE access for " + pkg + " with signature:"); + for (Signature s : info.signatures) { + if (s != null) { + Log.w(TAG, s.toCharsString()); + } + } + } + } catch (NameNotFoundException e) { + // ignore + } + return false; + } + + /** + * Parse nfcee_access.xml, populate mNfceeAccess + * Policy is to ignore unexpected XML elements and continue processing, + * except for obvious errors within a group since they might cause + * package names to by ignored and therefore wildcard access granted + * by mistake. Those errors invalidate the entire group. + */ + boolean parseNfceeAccess() { + File file = new File(Environment.getRootDirectory(), NFCEE_ACCESS_PATH); + FileReader reader = null; + boolean debug = false; + try { + reader = new FileReader(file); + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + XmlPullParser parser = factory.newPullParser(); + parser.setInput(reader); + + int event; + ArrayList packages = new ArrayList(); + Signature signature = null; + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); + while (true) { + event = parser.next(); + String tag = parser.getName(); + if (event == XmlPullParser.START_TAG && "signer".equals(tag)) { + signature = null; + packages.clear(); + for (int i = 0; i < parser.getAttributeCount(); i++) { + if ("android:signature".equals(parser.getAttributeName(i))) { + signature = new Signature(parser.getAttributeValue(i)); + break; + } + } + if (signature == null) { + Log.w(TAG, "signer tag is missing android:signature attribute, igorning"); + continue; + } + if (mNfceeAccess.containsKey(signature)) { + Log.w(TAG, "duplicate signature, ignoring"); + signature = null; + continue; + } + } else if (event == XmlPullParser.END_TAG && "signer".equals(tag)) { + if (signature == null) { + Log.w(TAG, "mis-matched signer tag"); + continue; + } + mNfceeAccess.put(signature, (String[])packages.toArray(new String[0])); + packages.clear(); + } else if (event == XmlPullParser.START_TAG && "package".equals(tag)) { + if (signature == null) { + Log.w(TAG, "ignoring unnested packge tag"); + continue; + } + String name = null; + for (int i = 0; i < parser.getAttributeCount(); i++) { + if ("android:name".equals(parser.getAttributeName(i))) { + name = parser.getAttributeValue(i); + break; + } + } + if (name == null) { + Log.w(TAG, "package missing android:name, ignoring signer group"); + signature = null; // invalidate signer + continue; + } + // check for duplicate package names + if (packages.contains(name)) { + Log.w(TAG, "duplicate package name in signer group, ignoring"); + continue; + } + packages.add(name); + } else if (event == XmlPullParser.START_TAG && "debug".equals(tag)) { + debug = true; + } else if (event == XmlPullParser.END_DOCUMENT) { + break; + } + } + } catch (XmlPullParserException e) { + Log.w(TAG, "failed to load NFCEE access list", e); + mNfceeAccess.clear(); // invalidate entire access list + } catch (FileNotFoundException e) { + Log.w(TAG, "could not find " + NFCEE_ACCESS_PATH + ", no NFCEE access allowed"); + } catch (IOException e) { + Log.e(TAG, "Failed to load NFCEE access list", e); + mNfceeAccess.clear(); // invalidate entire access list + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e2) { } + } + } + Log.i(TAG, "read " + mNfceeAccess.size() + " signature(s) for NFCEE access"); + return debug; + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("mNfceeAccess="); + for (Signature s : mNfceeAccess.keySet()) { + pw.printf("\t%s [", s.toCharsString()); + String[] ps = mNfceeAccess.get(s); + for (String p : ps) { + pw.printf("%s, ", p); + } + pw.println("]"); + } + synchronized (this) { + pw.println("mNfceeUidCache="); + for (Integer uid : mUidCache.keySet()) { + Boolean b = mUidCache.get(uid); + pw.printf("\t%d %s\n", uid, b); + } + } + } +} -- cgit v1.1