Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,6 @@ public void setPlayerListener(@Nullable final Consumer<Player> listener) {
public BrowserRoot onGetRoot(@NonNull final String clientPackageName,
final int clientUid,
@Nullable final Bundle rootHints) {
// TODO check if the accessing package has permission to view data
return mediaBrowserImpl.onGetRoot(clientPackageName, clientUid, rootHints);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import android.support.v4.media.MediaDescriptionCompat
import android.util.Log
import androidx.annotation.DrawableRes
import androidx.media.MediaBrowserServiceCompat
import androidx.media.MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT
import androidx.media.MediaBrowserServiceCompat.Result
import androidx.media.utils.MediaConstants
import io.reactivex.rxjava3.core.Flowable
Expand Down Expand Up @@ -47,6 +48,7 @@ class MediaBrowserImpl(
private val context: Context,
notifyChildrenChanged: Consumer<String>, // parentId
) {
private val packageValidator = PackageValidator(context)
private val database = NewPipeDatabase.getInstance(context)
private var disposables = CompositeDisposable()

Expand All @@ -68,11 +70,22 @@ class MediaBrowserImpl(
clientPackageName: String,
clientUid: Int,
rootHints: Bundle?
): MediaBrowserServiceCompat.BrowserRoot {
): MediaBrowserServiceCompat.BrowserRoot? {
if (DEBUG) {
Log.d(TAG, "onGetRoot($clientPackageName, $clientUid, $rootHints)")
}

if (!packageValidator.isKnownCaller(clientPackageName, clientUid)) {
// this is a caller we can't trust (see PackageValidator's rules taken from uamp)
return null
}

if (rootHints?.getBoolean(EXTRA_RECENT, false) == true) {
// the system is asking for a root to do media resumption, but we can't handle that yet,
// see https://developer.android.com/media/implement/surfaces/mobile#mediabrowserservice_implementation
return null
}

val extras = Bundle()
extras.putBoolean(
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
/*
* Copyright 2018 Google Inc. All rights reserved.
*
* 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.
*/

// THIS FILE WAS TAKEN FROM UAMP, EXCEPT FOR THINGS RELATED TO THE WHITELIST. UPDATE IT WHEN NEEDED.
// https://github.com/android/uamp/blob/329a21b63c247e9bd35f6858d4fc0e448fa38603/common/src/main/java/com/example/android/uamp/media/PackageValidator.kt

package org.schabi.newpipe.player.mediabrowser

import android.Manifest.permission.MEDIA_CONTENT_CONTROL
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED
import android.content.pm.PackageManager
import android.os.Process
import android.support.v4.media.session.MediaSessionCompat
import android.util.Log
import androidx.core.app.NotificationManagerCompat
import androidx.media.MediaBrowserServiceCompat
import org.schabi.newpipe.BuildConfig
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException

/**
* Validates that the calling package is authorized to browse a [MediaBrowserServiceCompat].
*
* The list of allowed signing certificates and their corresponding package names is defined in
* res/xml/allowed_media_browser_callers.xml.
*
* If you want to add a new caller to allowed_media_browser_callers.xml and you don't know
* its signature, this class will print to logcat (INFO level) a message with the proper
* xml tags to add to allow the caller.
*
* For more information, see res/xml/allowed_media_browser_callers.xml.
*/
internal class PackageValidator(context: Context) {
private val context: Context = context.applicationContext
private val packageManager: PackageManager = this.context.packageManager
private val platformSignature: String = getSystemSignature()
private val callerChecked = mutableMapOf<String, Pair<Int, Boolean>>()

/**
* Checks whether the caller attempting to connect to a [MediaBrowserServiceCompat] is known.
* See [MusicService.onGetRoot] for where this is utilized.
*
* @param callingPackage The package name of the caller.
* @param callingUid The user id of the caller.
* @return `true` if the caller is known, `false` otherwise.
*/
fun isKnownCaller(callingPackage: String, callingUid: Int): Boolean {
// If the caller has already been checked, return the previous result here.
val (checkedUid, checkResult) = callerChecked[callingPackage] ?: Pair(0, false)
if (checkedUid == callingUid) {
return checkResult
}

/**
* Because some of these checks can be slow, we save the results in [callerChecked] after
* this code is run.
*
* In particular, there's little reason to recompute the calling package's certificate
* signature (SHA-256) each call.
*
* This is safe to do as we know the UID matches the package's UID (from the check above),
* and app UIDs are set at install time. Additionally, a package name + UID is guaranteed to
* be constant until a reboot. (After a reboot then a previously assigned UID could be
* reassigned.)
*/

// Build the caller info for the rest of the checks here.
val callerPackageInfo = buildCallerInfo(callingPackage)
?: throw IllegalStateException("Caller wasn't found in the system?")

// Verify that things aren't ... broken. (This test should always pass.)
if (callerPackageInfo.uid != callingUid) {
throw IllegalStateException("Caller's package UID doesn't match caller's actual UID?")
}

val callerSignature = callerPackageInfo.signature

val isCallerKnown = when {
// If it's our own app making the call, allow it.
callingUid == Process.myUid() -> true
// If the system is making the call, allow it.
callingUid == Process.SYSTEM_UID -> true
// If the app was signed by the same certificate as the platform itself, also allow it.
callerSignature == platformSignature -> true
/**
* [MEDIA_CONTENT_CONTROL] permission is only available to system applications, and
* while it isn't required to allow these apps to connect to a
* [MediaBrowserServiceCompat], allowing this ensures optimal compatability with apps
* such as Android TV and the Google Assistant.
*/
callerPackageInfo.permissions.contains(MEDIA_CONTENT_CONTROL) -> true
/**
* If the calling app has a notification listener it is able to retrieve notifications
* and can connect to an active [MediaSessionCompat].
*
* It's not required to allow apps with a notification listener to
* connect to your [MediaBrowserServiceCompat], but it does allow easy compatibility
* with apps such as Wear OS.
*/
NotificationManagerCompat.getEnabledListenerPackages(this.context)
.contains(callerPackageInfo.packageName) -> true

// If none of the previous checks succeeded, then the caller is unrecognized.
else -> false
}

if (!isCallerKnown) {
logUnknownCaller(callerPackageInfo)
}

// Save our work for next time.
callerChecked[callingPackage] = Pair(callingUid, isCallerKnown)
return isCallerKnown
}

/**
* Logs an info level message with details of how to add a caller to the allowed callers list
* when the app is debuggable.
*/
private fun logUnknownCaller(callerPackageInfo: CallerPackageInfo) {
if (BuildConfig.DEBUG) {
Log.w(TAG, "Unknown caller $callerPackageInfo")
}
}

/**
* Builds a [CallerPackageInfo] for a given package that can be used for all the
* various checks that are performed before allowing an app to connect to a
* [MediaBrowserServiceCompat].
*/
private fun buildCallerInfo(callingPackage: String): CallerPackageInfo? {
val packageInfo = getPackageInfo(callingPackage) ?: return null

val appName = packageInfo.applicationInfo.loadLabel(packageManager).toString()
val uid = packageInfo.applicationInfo.uid
val signature = getSignature(packageInfo)

val requestedPermissions = packageInfo.requestedPermissions
val permissionFlags = packageInfo.requestedPermissionsFlags
val activePermissions = mutableSetOf<String>()
requestedPermissions?.forEachIndexed { index, permission ->
if (permissionFlags[index] and REQUESTED_PERMISSION_GRANTED != 0) {
activePermissions += permission
}
}

return CallerPackageInfo(appName, callingPackage, uid, signature, activePermissions.toSet())
}

/**
* Looks up the [PackageInfo] for a package name.
* This requests both the signatures (for checking if an app is on the allow list) and
* the app's permissions, which allow for more flexibility in the allow list.
*
* @return [PackageInfo] for the package name or null if it's not found.
*/
@Suppress("deprecation")
@SuppressLint("PackageManagerGetSignatures")
private fun getPackageInfo(callingPackage: String): PackageInfo? =
packageManager.getPackageInfo(
callingPackage,
PackageManager.GET_SIGNATURES or PackageManager.GET_PERMISSIONS
)

/**
* Gets the signature of a given package's [PackageInfo].
*
* The "signature" is a SHA-256 hash of the public key of the signing certificate used by
* the app.
*
* If the app is not found, or if the app does not have exactly one signature, this method
* returns `null` as the signature.
*/
@Suppress("deprecation")
private fun getSignature(packageInfo: PackageInfo): String? =
if (packageInfo.signatures == null || packageInfo.signatures.size != 1) {
// Security best practices dictate that an app should be signed with exactly one (1)
// signature. Because of this, if there are multiple signatures, reject it.
null
} else {
val certificate = packageInfo.signatures[0].toByteArray()
getSignatureSha256(certificate)
}

/**
* Finds the Android platform signing key signature. This key is never null.
*/
private fun getSystemSignature(): String =
getPackageInfo(ANDROID_PLATFORM)?.let { platformInfo ->
getSignature(platformInfo)
} ?: throw IllegalStateException("Platform signature not found")

/**
* Creates a SHA-256 signature given a certificate byte array.
*/
private fun getSignatureSha256(certificate: ByteArray): String {
val md: MessageDigest
try {
md = MessageDigest.getInstance("SHA256")
} catch (noSuchAlgorithmException: NoSuchAlgorithmException) {
Log.e(TAG, "No such algorithm: $noSuchAlgorithmException")
throw RuntimeException("Could not find SHA256 hash algorithm", noSuchAlgorithmException)
}
md.update(certificate)

// This code takes the byte array generated by `md.digest()` and joins each of the bytes
// to a string, applying the string format `%02x` on each digit before it's appended, with
// a colon (':') between each of the items.
// For example: input=[0,2,4,6,8,10,12], output="00:02:04:06:08:0a:0c"
return md.digest().joinToString(":") { String.format("%02x", it) }
}

/**
* Convenience class to hold all of the information about an app that's being checked
* to see if it's a known caller.
*/
private data class CallerPackageInfo(
val name: String,
val packageName: String,
val uid: Int,
val signature: String?,
val permissions: Set<String>
)
}

private const val TAG = "PackageValidator"
private const val ANDROID_PLATFORM = "android"