1/*
2 * Copyright (C) 2011 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
17package com.android.nfc;
18
19import java.io.File;
20import java.io.FileDescriptor;
21import java.io.FileNotFoundException;
22import java.io.FileReader;
23import java.io.IOException;
24import java.io.PrintWriter;
25import java.util.ArrayList;
26import java.util.HashMap;
27
28import org.xmlpull.v1.XmlPullParser;
29import org.xmlpull.v1.XmlPullParserException;
30import org.xmlpull.v1.XmlPullParserFactory;
31
32import android.content.Context;
33import android.content.pm.ApplicationInfo;
34import android.content.pm.PackageInfo;
35import android.content.pm.PackageManager;
36import android.content.pm.Signature;
37import android.content.pm.PackageManager.NameNotFoundException;
38import android.os.Environment;
39import android.util.Log;
40
41public class NfceeAccessControl {
42    static final String TAG = "NfceeAccess";
43    static final boolean DBG = true;
44
45    public static final String NFCEE_ACCESS_PATH = "/etc/nfcee_access.xml";
46
47    /**
48     * Map of signatures to valid packages names, as read from nfcee_access.xml.
49     * An empty list of package names indicates that any package
50     * with this signature is allowed.
51     */
52    final HashMap<Signature, String[]> mNfceeAccess;  // contents final after onCreate()
53
54    /**
55     * Map from UID to NFCEE access, used as a cache.
56     * Note: if a UID contains multiple packages they must all be
57     * signed with the same certificate so in effect UID == certificate
58     * used to sign the package.
59     */
60    final HashMap<Integer, Boolean> mUidCache;  // contents guarded by this
61
62    final Context mContext;
63    final boolean mDebugPrintSignature;
64
65    NfceeAccessControl(Context context) {
66        mContext = context;
67        mNfceeAccess = new HashMap<Signature, String[]>();
68        mUidCache = new HashMap<Integer, Boolean>();
69        mDebugPrintSignature = parseNfceeAccess();
70    }
71
72    /**
73     * Check if the {uid, pkg} combination may use NFCEE.
74     * Also verify with package manager that this {uid, pkg} combination
75     * is valid if it is not cached.
76     */
77    public boolean check(int uid, String pkg) {
78        synchronized (this) {
79            Boolean cached = mUidCache.get(uid);
80            if (cached != null) {
81                return cached;
82            }
83
84            boolean access = false;
85
86            // Ensure the claimed package is present in the calling UID
87            PackageManager pm = mContext.getPackageManager();
88            String[] pkgs = pm.getPackagesForUid(uid);
89            for (String uidPkg : pkgs) {
90                if (uidPkg.equals(pkg)) {
91                    // Ensure the package has access permissions
92                    if (checkPackageNfceeAccess(pkg)) {
93                        access = true;
94                    }
95                    break;
96                }
97            }
98
99            mUidCache.put(uid, access);
100            return access;
101        }
102    }
103
104    /**
105     * Check if the given ApplicationInfo may use the NFCEE.
106     * Assumes ApplicationInfo came from package manager,
107     * so no need to confirm {uid, pkg} is valid.
108     */
109    public boolean check(ApplicationInfo info) {
110        synchronized (this) {
111            Boolean access = mUidCache.get(info.uid);
112            if (access == null) {
113                access = checkPackageNfceeAccess(info.packageName);
114                mUidCache.put(info.uid, access);
115            }
116            return access;
117        }
118    }
119
120    public void invalidateCache() {
121        synchronized (this) {
122            mUidCache.clear();
123        }
124    }
125
126    /**
127     * Check with package manager if the pkg may use NFCEE.
128     * Does not use cache.
129     */
130    boolean checkPackageNfceeAccess(String pkg) {
131        PackageManager pm = mContext.getPackageManager();
132        try {
133            PackageInfo info = pm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES);
134            if (info.signatures == null) {
135                return false;
136            }
137
138            for (Signature s : info.signatures){
139                if (s == null) {
140                    continue;
141                }
142                String[] packages = mNfceeAccess.get(s);
143                if (packages == null) {
144                    continue;
145                }
146                if (packages.length == 0) {
147                    // wildcard access
148                    if (DBG) Log.d(TAG, "Granted NFCEE access to " + pkg + " (wildcard)");
149                    return true;
150                }
151                for (String p : packages) {
152                    if (pkg.equals(p)) {
153                        // explicit package access
154                        if (DBG) Log.d(TAG, "Granted access to " + pkg + " (explicit)");
155                        return true;
156                    }
157                }
158            }
159
160            if (mDebugPrintSignature) {
161                Log.w(TAG, "denied NFCEE access for " + pkg + " with signature:");
162                for (Signature s : info.signatures) {
163                    if (s != null) {
164                        Log.w(TAG, s.toCharsString());
165                    }
166                }
167            }
168        } catch (NameNotFoundException e) {
169            // ignore
170        }
171        return false;
172    }
173
174    /**
175     * Parse nfcee_access.xml, populate mNfceeAccess
176     * Policy is to ignore unexpected XML elements and continue processing,
177     * except for obvious errors within a <signer> group since they might cause
178     * package names to by ignored and therefore wildcard access granted
179     * by mistake. Those errors invalidate the entire <signer> group.
180     */
181    boolean parseNfceeAccess() {
182        File file = new File(Environment.getRootDirectory(), NFCEE_ACCESS_PATH);
183        FileReader reader = null;
184        boolean debug = false;
185        try {
186            reader = new FileReader(file);
187            XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
188            XmlPullParser parser = factory.newPullParser();
189            parser.setInput(reader);
190
191            int event;
192            ArrayList<String> packages = new ArrayList<String>();
193            Signature signature = null;
194            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
195            while (true) {
196                event = parser.next();
197                String tag = parser.getName();
198                if (event == XmlPullParser.START_TAG && "signer".equals(tag)) {
199                    signature = null;
200                    packages.clear();
201                    for (int i = 0; i < parser.getAttributeCount(); i++) {
202                        if ("android:signature".equals(parser.getAttributeName(i))) {
203                            signature = new Signature(parser.getAttributeValue(i));
204                            break;
205                        }
206                    }
207                    if (signature == null) {
208                        Log.w(TAG, "signer tag is missing android:signature attribute, igorning");
209                        continue;
210                    }
211                    if (mNfceeAccess.containsKey(signature)) {
212                        Log.w(TAG, "duplicate signature, ignoring");
213                        signature = null;
214                        continue;
215                    }
216                } else if (event == XmlPullParser.END_TAG && "signer".equals(tag)) {
217                    if (signature == null) {
218                        Log.w(TAG, "mis-matched signer tag");
219                        continue;
220                    }
221                    mNfceeAccess.put(signature, packages.toArray(new String[0]));
222                    packages.clear();
223                } else if (event == XmlPullParser.START_TAG && "package".equals(tag)) {
224                    if (signature == null) {
225                        Log.w(TAG, "ignoring unnested packge tag");
226                        continue;
227                    }
228                    String name = null;
229                    for (int i = 0; i < parser.getAttributeCount(); i++) {
230                        if ("android:name".equals(parser.getAttributeName(i))) {
231                            name = parser.getAttributeValue(i);
232                            break;
233                        }
234                    }
235                    if (name == null) {
236                        Log.w(TAG, "package missing android:name, ignoring signer group");
237                        signature = null;  // invalidate signer
238                        continue;
239                    }
240                    // check for duplicate package names
241                    if (packages.contains(name)) {
242                        Log.w(TAG, "duplicate package name in signer group, ignoring");
243                        continue;
244                    }
245                    packages.add(name);
246                } else if (event == XmlPullParser.START_TAG && "debug".equals(tag)) {
247                    debug = true;
248                } else if (event == XmlPullParser.END_DOCUMENT) {
249                    break;
250                }
251            }
252        } catch (XmlPullParserException e) {
253            Log.w(TAG, "failed to load NFCEE access list", e);
254            mNfceeAccess.clear();  // invalidate entire access list
255        } catch (FileNotFoundException e) {
256            Log.w(TAG, "could not find " + NFCEE_ACCESS_PATH + ", no NFCEE access allowed");
257        } catch (IOException e) {
258            Log.e(TAG, "Failed to load NFCEE access list", e);
259            mNfceeAccess.clear();  // invalidate entire access list
260        } finally {
261            if (reader != null) {
262                try {
263                    reader.close();
264                } catch (IOException e2)  { }
265            }
266        }
267        Log.i(TAG, "read " + mNfceeAccess.size() + " signature(s) for NFCEE access");
268        return debug;
269    }
270
271    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
272        pw.println("mNfceeAccess=");
273        for (Signature s : mNfceeAccess.keySet()) {
274            pw.printf("\t%s [", s.toCharsString());
275            String[] ps = mNfceeAccess.get(s);
276            for (String p : ps) {
277                pw.printf("%s, ", p);
278            }
279            pw.println("]");
280        }
281        synchronized (this) {
282            pw.println("mNfceeUidCache=");
283            for (Integer uid : mUidCache.keySet()) {
284                Boolean b = mUidCache.get(uid);
285                pw.printf("\t%d %s\n", uid, b);
286            }
287        }
288    }
289}
290