diff options
-rwxr-xr-x | AndroidManifest.xml | 2 | ||||
-rw-r--r-- | etc/sample_nfcee_access.xml | 45 | ||||
-rwxr-xr-x | res/values/strings.xml | 7 | ||||
-rwxr-xr-x | src/com/android/nfc/NfcService.java | 85 | ||||
-rw-r--r-- | src/com/android/nfc/NfceeAccessControl.java | 289 |
5 files changed, 393 insertions, 35 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 38dd575..6177da4 100755 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -23,7 +23,7 @@ <permission android:name="com.android.nfc.permission.NFCEE_ADMIN" android:label="@string/permlab_nfcAdmin" android:description="@string/permdesc_nfcAdmin" - android:protectionLevel="signature" + android:protectionLevel="dangerous" /> <application android:name="com.android.nfc.NfcService" diff --git a/etc/sample_nfcee_access.xml b/etc/sample_nfcee_access.xml new file mode 100644 index 0000000..d6c8707 --- /dev/null +++ b/etc/sample_nfcee_access.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- The built in list of signatures and package names that are allowed + access to the NFCEE (Secure Element). + + Format: + <signer android:signature="SIGNATURE"> + <package android:name="PACKAGE_NAME" /> + ... + </signer> + ... + + Rules: + SIGNATURE is a hex encoded X.509 certificate + See Debug section to generate hex encoded certificates. + PACKAGE_NAME is a Android package name + Zero or more signer tags are allowed. + Each signer tag must have one android:signature. + Zero or more package tags are allowed per signer. + Each package tag must have one android:name. + If a signer has zero package tags, then ANY application with the + specified certificate is granted NFCEE access. + If a signer has one or more package tags, then an application signed + with the specified certificate must have a package name that is an + exact match of one of the specified package names to be granted + NFCEE access. + Duplicate signer tags are not allowed. + Duplicate package tags for a single signer are not allowed. + + Example: + In this example, any application from the first signer is granted + access, but only applications with the specified package names + are granted access for the second signer. + <signer android:signature="308201c53082012ea00302010202044ebb27cc300d06092a864886f70d01010505003026310f300d060355040a1306476f6f676c65311330110603550403130a4e69636b2050656c6c793020170d3131313131303031323432385a180f32313131313031373031323432385a3026310f300d060355040a1306476f6f676c65311330110603550403130a4e69636b2050656c6c7930819f300d06092a864886f70d010101050003818d00308189028181008d43e546b3f5572707a095ced120d8f06781fa162bcf0ffa0ed0ecb48eb90ed009f65a5a1afd69fb4d38cf24e931b69b061741b8c7ca9f785ba59509e883f5a308f5e2da3c496bb362a2229da8f95f08a92f7f94c829c56e78a34e5147d138d0be0671cb5b7caceaffae6199ba544496a7645e7df3c9f02c5ac156eb0501584f0203010001300d06092a864886f70d0101050500038181003511bcb73651a7927db71ad76e4f6dc5ba121c941ae0fd4dfe519aae8775520b204a5e3cdad2c61ad41aff2c510dbe3376a7578d8aba00f35633e1ae72c91ec83d80eac6f5f081fb8361c6c30c47b0a17932d859d7e991e02cba410a82b35234b2b1bc859e50fe308bf9b64b71a8046777300c07ead159287c187b8865e23f23" /> + <signer android:signature="3082044c30820334a003020102020900de7695041d7650c0300d06092a864886f70d01010505003077310b3009060355040613025553311330110603550408130a43616c69666f726e6961311630140603550407130d4d6f756e7461696e205669657731143012060355040a130b476f6f676c6520496e632e3110300e060355040b1307416e64726f6964311330110603550403130a476f6f676c65204e4643301e170d3131303332343031303332345a170d3338303830393031303332345a3077310b3009060355040613025553311330110603550408130a43616c69666f726e6961311630140603550407130d4d6f756e7461696e205669657731143012060355040a130b476f6f676c6520496e632e3110300e060355040b1307416e64726f6964311330110603550403130a476f6f676c65204e464330820120300d06092a864886f70d01010105000382010d00308201080282010100e6ff3defe92aa10d71eb0fa6408bc036b7e243eeed68a6a4763dc7a52a31757cdac61fe510bb73c716e4000104265b347fcecef4c42bf1e1379dd0a876f028227fbbc1f9bdd5d713b2f6a935a379d2cba9c96f92d2d0787c11f1eb19548008a6a072b34b91836cfa0ae1276780e9007530166986a11c9cef46cef7c704806dde9431fb60284d120ab0e7de1d633f07687d468c51139afffdc6bc9a207ca904b8be1da0aa7b4e97756f43606488be5cae3c68e8bb7942cdf51607c930a2fcda655b75d0759cba89ad06e739bd0ba29b1f404296c2c0a85a847f5ab0d067c6c3ec9c49212042ac63a7e53b546c65b46080b4e3e680e23e1f77cfe7f6de744b1a65020103a381dc3081d9301d0603551d0e04160414a2e89064b05d08865c34db930a9d840050117aec3081a90603551d230481a130819e8014a2e89064b05d08865c34db930a9d840050117aeca17ba4793077310b3009060355040613025553311330110603550408130a43616c69666f726e6961311630140603550407130d4d6f756e7461696e205669657731143012060355040a130b476f6f676c6520496e632e3110300e060355040b1307416e64726f6964311330110603550403130a476f6f676c65204e4643820900de7695041d7650c0300c0603551d13040530030101ff300d06092a864886f70d010105050003820101003771870ce87c3c52ea84899230c6e962d94b4d5f1293c25d88261541fd90b5555d1285cef3b8312c3f5df691a8aae04cb981b305e427fd1d2d9e1987e1d29078f13c8452990f1821980263d8d4bd36519348d8d8ba26d8b99fbf09f5fd3ebb0ea3c2f0c9376f1e1fca76f3a6a405429d081b752a7a90b756e9ab44da41abc8e1e8f88ac2758da743fb73e650719a57840ccb6b7add21b99fc681e456e1872c223d5c074adf55f6abda268c2d8b64ea0a8845eecd968f92b493127e75c753c3ff30cbc678b51c9f52961472f17da20a0dc6274aa2463434c1a9b614df697d8ff5ca8101e7a25c7db3fb055d65569c04b01d389cabba57b3a1703ec2e74a88d334"> + <package android:name="com.foo.my.awesome.wallet" /> + <package android:name="com.foo.my.awesome.wallet.tests" /> + </signer> + + Debug: + Including the tag <debug /> will print to logcat the + hex encoded signature of applications that are denied NFCEE + access, for easy cut-and-paste into this file. + --> +</resources> diff --git a/res/values/strings.xml b/res/values/strings.xml index ceb2128..0963dc7 100755 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -2,13 +2,6 @@ <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="app_name">Nfc Service</string> <string name="nfcUserLabel">Nfc</string> - - <!-- The label for the NFC admin permission --> - <string name="permlab_nfcAdmin">control Near Field Communication secure hardware</string> - - <!-- The description for the NFC admin permission --> - <string name="permdesc_nfcAdmin">Allows this application to control the secure hardware used with Near Field Communication, - for example, to modify payment credentials.</string> <!-- A notification description string informing the user that contact details were received over NFC [CHAR-LIMIT=64] --> <string name="inbound_me_profile_title">Contact received over NFC</string> 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<ResolveInfo> 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<Signature, String[]> 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<Integer, Boolean> mUidCache; // contents guarded by this + + final Context mContext; + final boolean mDebugPrintSignature; + + NfceeAccessControl(Context context) { + mContext = context; + mNfceeAccess = new HashMap<Signature, String[]>(); + mUidCache = new HashMap<Integer, Boolean>(); + 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 <signer> group since they might cause + * package names to by ignored and therefore wildcard access granted + * by mistake. Those errors invalidate the entire <signer> 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<String> packages = new ArrayList<String>(); + 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); + } + } + } +} |