1/*
2 * Copyright (C) 2015 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.example.android.supportv4.media;
18
19import android.content.Context;
20import android.content.pm.PackageInfo;
21import android.content.pm.PackageManager;
22import android.content.res.XmlResourceParser;
23import android.os.Process;
24import android.util.Base64;
25import android.util.Log;
26
27import com.example.android.supportv4.R;
28
29import org.xmlpull.v1.XmlPullParserException;
30
31import java.io.IOException;
32import java.util.ArrayList;
33import java.util.HashMap;
34import java.util.Map;
35
36/**
37 * Validates that the calling package is authorized to browse a
38 * {@link android.service.media.MediaBrowserService}.
39 *
40 * The list of allowed signing certificates and their corresponding package names is defined in
41 * res/xml/allowed_media_browser_callers.xml.
42 */
43public class PackageValidator {
44    private static final String TAG = "PackageValidator";
45
46    /**
47     * Map allowed callers' certificate keys to the expected caller information.
48     *
49     */
50    private final Map<String, ArrayList<CallerInfo>> mValidCertificates;
51
52    public PackageValidator(Context ctx) {
53        mValidCertificates = readValidCertificates(ctx.getResources().getXml(
54            R.xml.allowed_media_browser_callers));
55    }
56
57    private Map<String, ArrayList<CallerInfo>> readValidCertificates(XmlResourceParser parser) {
58        HashMap<String, ArrayList<CallerInfo>> validCertificates = new HashMap<>();
59        try {
60            int eventType = parser.next();
61            while (eventType != XmlResourceParser.END_DOCUMENT) {
62                if (eventType == XmlResourceParser.START_TAG
63                        && parser.getName().equals("signing_certificate")) {
64
65                    String name = parser.getAttributeValue(null, "name");
66                    String packageName = parser.getAttributeValue(null, "package");
67                    boolean isRelease = parser.getAttributeBooleanValue(null, "release", false);
68                    String certificate = parser.nextText().replaceAll("\\s|\\n", "");
69
70                    CallerInfo info = new CallerInfo(name, packageName, isRelease, certificate);
71
72                    ArrayList<CallerInfo> infos = validCertificates.get(certificate);
73                    if (infos == null) {
74                        infos = new ArrayList<>();
75                        validCertificates.put(certificate, infos);
76                    }
77                    Log.v(TAG, "Adding allowed caller: " + info.name + " package="
78                            + info.packageName + " release=" + info.release + " certificate="
79                            + certificate);
80                    infos.add(info);
81                }
82                eventType = parser.next();
83            }
84        } catch (XmlPullParserException | IOException e) {
85            Log.e(TAG, "Could not read allowed callers from XML.", e);
86        }
87        return validCertificates;
88    }
89
90    /**
91     * @return false if the caller is not authorized to get data from this MediaBrowserService
92     */
93    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
94    public boolean isCallerAllowed(Context context, String callingPackage, int callingUid) {
95        // Always allow calls from the framework, self app or development environment.
96        if (Process.SYSTEM_UID == callingUid || Process.myUid() == callingUid) {
97            return true;
98        }
99        PackageManager packageManager = context.getPackageManager();
100        PackageInfo packageInfo;
101        try {
102            packageInfo = packageManager.getPackageInfo(
103                    callingPackage, PackageManager.GET_SIGNATURES);
104        } catch (PackageManager.NameNotFoundException e) {
105            Log.w(TAG, "Package manager can't find package: " + callingPackage, e);
106            return false;
107        }
108        if (packageInfo.signatures.length != 1) {
109            Log.w(TAG, "Caller has more than one signature certificate!");
110            return false;
111        }
112        String signature = Base64.encodeToString(
113            packageInfo.signatures[0].toByteArray(), Base64.NO_WRAP);
114
115        // Test for known signatures:
116        ArrayList<CallerInfo> validCallers = mValidCertificates.get(signature);
117        if (validCallers == null) {
118            Log.v(TAG, "Signature for caller " + callingPackage + " is not valid: \n" + signature);
119            if (mValidCertificates.isEmpty()) {
120                Log.w(TAG, "The list of valid certificates is empty. Either your file"
121                        + " res/xml/allowed_media_browser_callers.xml is empty or there was an"
122                        + " error while reading it. Check previous log messages.");
123            }
124            return false;
125        }
126
127        // Check if the package name is valid for the certificate:
128        StringBuffer expectedPackages = new StringBuffer();
129        for (CallerInfo info: validCallers) {
130            if (callingPackage.equals(info.packageName)) {
131                Log.v(TAG, "Valid caller: " + info.name + "  package=" + info.packageName
132                    + " release=" + info.release);
133                return true;
134            }
135            expectedPackages.append(info.packageName).append(' ');
136        }
137
138        Log.i(TAG, "Caller has a valid certificate, but its package doesn't match any expected"
139                + "package for the given certificate. Caller's package is " + callingPackage
140                + ". Expected packages as defined in res/xml/allowed_media_browser_callers.xml are"
141                +" (" + expectedPackages + "). This caller's certificate is: \n" + signature);
142
143        return false;
144    }
145
146    private final static class CallerInfo {
147        final String name;
148        final String packageName;
149        final boolean release;
150        final String signingCertificate;
151
152        public CallerInfo(String name, String packageName, boolean release,
153                          String signingCertificate) {
154            this.name = name;
155            this.packageName = packageName;
156            this.release = release;
157            this.signingCertificate = signingCertificate;
158        }
159    }
160}
161