mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 07:13:00 +00:00 
			
		
		
		
	Merge pull request #12412 from Stypox/fix-ghost-notifications
This commit is contained in:
		| @@ -330,7 +330,6 @@ public final class PlayerService extends MediaBrowserServiceCompat { | |||||||
|     public BrowserRoot onGetRoot(@NonNull final String clientPackageName, |     public BrowserRoot onGetRoot(@NonNull final String clientPackageName, | ||||||
|                                  final int clientUid, |                                  final int clientUid, | ||||||
|                                  @Nullable final Bundle rootHints) { |                                  @Nullable final Bundle rootHints) { | ||||||
|         // TODO check if the accessing package has permission to view data |  | ||||||
|         return mediaBrowserImpl.onGetRoot(clientPackageName, clientUid, rootHints); |         return mediaBrowserImpl.onGetRoot(clientPackageName, clientUid, rootHints); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import android.util.Log | |||||||
| import androidx.annotation.DrawableRes | import androidx.annotation.DrawableRes | ||||||
| import androidx.core.net.toUri | import androidx.core.net.toUri | ||||||
| import androidx.media.MediaBrowserServiceCompat | import androidx.media.MediaBrowserServiceCompat | ||||||
|  | import androidx.media.MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT | ||||||
| import androidx.media.MediaBrowserServiceCompat.Result | import androidx.media.MediaBrowserServiceCompat.Result | ||||||
| import androidx.media.utils.MediaConstants | import androidx.media.utils.MediaConstants | ||||||
| import io.reactivex.rxjava3.core.Flowable | import io.reactivex.rxjava3.core.Flowable | ||||||
| @@ -48,6 +49,7 @@ class MediaBrowserImpl( | |||||||
|     private val context: Context, |     private val context: Context, | ||||||
|     notifyChildrenChanged: Consumer<String>, // parentId |     notifyChildrenChanged: Consumer<String>, // parentId | ||||||
| ) { | ) { | ||||||
|  |     private val packageValidator = PackageValidator(context) | ||||||
|     private val database = NewPipeDatabase.getInstance(context) |     private val database = NewPipeDatabase.getInstance(context) | ||||||
|     private var disposables = CompositeDisposable() |     private var disposables = CompositeDisposable() | ||||||
|  |  | ||||||
| @@ -69,11 +71,22 @@ class MediaBrowserImpl( | |||||||
|         clientPackageName: String, |         clientPackageName: String, | ||||||
|         clientUid: Int, |         clientUid: Int, | ||||||
|         rootHints: Bundle? |         rootHints: Bundle? | ||||||
|     ): MediaBrowserServiceCompat.BrowserRoot { |     ): MediaBrowserServiceCompat.BrowserRoot? { | ||||||
|         if (DEBUG) { |         if (DEBUG) { | ||||||
|             Log.d(TAG, "onGetRoot($clientPackageName, $clientUid, $rootHints)") |             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() |         val extras = Bundle() | ||||||
|         extras.putBoolean( |         extras.putBoolean( | ||||||
|             MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true |             MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true | ||||||
|   | |||||||
| @@ -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" | ||||||
		Reference in New Issue
	
	Block a user
	 Stypox
					Stypox