/* * Copyright (C) 2020 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.server.soundtrigger_middleware; import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD; import static android.Manifest.permission.RECORD_AUDIO; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.PermissionChecker; import android.media.permission.Identity; import android.media.permission.IdentityContext; import android.media.permission.PermissionUtil; import android.media.soundtrigger.ModelParameterRange; import android.media.soundtrigger.PhraseSoundModel; import android.media.soundtrigger.RecognitionConfig; import android.media.soundtrigger.SoundModel; import android.media.soundtrigger.Status; import android.media.soundtrigger_middleware.ISoundTriggerCallback; import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService; import android.media.soundtrigger_middleware.ISoundTriggerModule; import android.media.soundtrigger_middleware.PhraseRecognitionEventSys; import android.media.soundtrigger_middleware.RecognitionEventSys; import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceSpecificException; import com.android.server.LocalServices; import com.android.server.pm.permission.LegacyPermissionManagerInternal; import java.io.PrintWriter; import java.util.Objects; /** * This is a decorator of an {@link ISoundTriggerMiddlewareService}, which enforces permissions. *

* Every public method in this class, overriding an interface method, must follow a similar * pattern: *

 * @Override public T method(S arg) {
 *     // Permission check.
 *     enforcePermissions*(...);
 *     return mDelegate.method(arg);
 * }
 * 
* * {@hide} */ public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddlewareInternal, Dumpable { private static final String TAG = "SoundTriggerMiddlewarePermission"; private final @NonNull ISoundTriggerMiddlewareInternal mDelegate; private final @NonNull Context mContext; public SoundTriggerMiddlewarePermission( @NonNull ISoundTriggerMiddlewareInternal delegate, @NonNull Context context) { mDelegate = delegate; mContext = context; } @Override public @NonNull SoundTriggerModuleDescriptor[] listModules() { Identity identity = getIdentity(); enforcePermissionForPreflight(mContext, identity, CAPTURE_AUDIO_HOTWORD); return mDelegate.listModules(); } @Override public @NonNull ISoundTriggerModule attach(int handle, @NonNull ISoundTriggerCallback callback, boolean isTrusted) { Identity identity = getIdentity(); enforcePermissionsForPreflight(identity); ModuleWrapper wrapper = new ModuleWrapper(identity, callback, isTrusted); return wrapper.attach(mDelegate.attach(handle, wrapper.getCallbackWrapper(), isTrusted)); } // Override toString() in order to have the delegate's ID in it. @Override public String toString() { return Objects.toString(mDelegate); } /** * Get the identity context, or throws an InternalServerError if it has not been established. * * @return The identity. */ private static @NonNull Identity getIdentity() { return IdentityContext.getNonNull(); } /** * Throws a {@link SecurityException} if originator permanently doesn't have the given * permission, * or a {@link ServiceSpecificException} with a {@link Status#TEMPORARY_PERMISSION_DENIED} if * originator temporarily doesn't have the right permissions to use this service. */ private void enforcePermissionsForPreflight(@NonNull Identity identity) { enforcePermissionForPreflight(mContext, identity, RECORD_AUDIO); enforcePermissionForPreflight(mContext, identity, CAPTURE_AUDIO_HOTWORD); } /** * Throws a {@link SecurityException} iff the originator has permission to receive data. */ void enforcePermissionsForDataDelivery(@NonNull Identity identity, @NonNull String reason) { enforceSoundTriggerRecordAudioPermissionForDataDelivery(identity, reason); enforcePermissionForDataDelivery(mContext, identity, CAPTURE_AUDIO_HOTWORD, reason); } /** * Throws a {@link SecurityException} iff the given identity has given permission to receive * data. * * @param context A {@link Context}, used for permission checks. * @param identity The identity to check. * @param permission The identifier of the permission we want to check. * @param reason The reason why we're requesting the permission, for auditing purposes. */ private static void enforcePermissionForDataDelivery(@NonNull Context context, @NonNull Identity identity, @NonNull String permission, @NonNull String reason) { final int status = PermissionUtil.checkPermissionForDataDelivery(context, identity, permission, reason); if (status != PermissionChecker.PERMISSION_GRANTED) { throw new SecurityException( String.format("Failed to obtain permission %s for identity %s", permission, ObjectPrinter.print(identity, 16))); } } private static void enforceSoundTriggerRecordAudioPermissionForDataDelivery( @NonNull Identity identity, @NonNull String reason) { LegacyPermissionManagerInternal lpmi = LocalServices.getService(LegacyPermissionManagerInternal.class); final int status = lpmi.checkSoundTriggerRecordAudioPermissionForDataDelivery(identity.uid, identity.packageName, identity.attributionTag, reason); if (status != PermissionChecker.PERMISSION_GRANTED) { throw new SecurityException( String.format("Failed to obtain permission RECORD_AUDIO for identity %s", ObjectPrinter.print(identity, 16))); } } /** * Throws a {@link SecurityException} if originator permanently doesn't have the given * permission. * Soft (temporary) denials are considered OK for preflight purposes. * * @param context A {@link Context}, used for permission checks. * @param identity The identity to check. * @param permission The identifier of the permission we want to check. */ private static void enforcePermissionForPreflight(@NonNull Context context, @NonNull Identity identity, @NonNull String permission) { final int status = PermissionUtil.checkPermissionForPreflight(context, identity, permission); switch (status) { case PermissionChecker.PERMISSION_GRANTED: case PermissionChecker.PERMISSION_SOFT_DENIED: return; case PermissionChecker.PERMISSION_HARD_DENIED: throw new SecurityException( String.format("Failed to obtain permission %s for identity %s", permission, ObjectPrinter.print(identity, 16))); default: throw new RuntimeException("Unexpected perimission check result."); } } @Override public void dump(PrintWriter pw) { if (mDelegate instanceof Dumpable) { ((Dumpable) mDelegate).dump(pw); } } /** * A wrapper around an {@link ISoundTriggerModule} implementation, to address the same aspects * mentioned in {@link SoundTriggerModule} above. This class follows the same conventions. */ private class ModuleWrapper extends ISoundTriggerModule.Stub { private ISoundTriggerModule mDelegate; private final @NonNull Identity mOriginatorIdentity; private final @NonNull CallbackWrapper mCallbackWrapper; private final boolean mIsTrusted; ModuleWrapper(@NonNull Identity originatorIdentity, @NonNull ISoundTriggerCallback callback, boolean isTrusted) { mOriginatorIdentity = originatorIdentity; mCallbackWrapper = new CallbackWrapper(callback); mIsTrusted = isTrusted; } ModuleWrapper attach(@NonNull ISoundTriggerModule delegate) { mDelegate = delegate; return this; } ISoundTriggerCallback getCallbackWrapper() { return mCallbackWrapper; } @Override public int loadModel(@NonNull SoundModel model) throws RemoteException { enforcePermissions(); return mDelegate.loadModel(model); } @Override public int loadPhraseModel(@NonNull PhraseSoundModel model) throws RemoteException { enforcePermissions(); return mDelegate.loadPhraseModel(model); } @Override public void unloadModel(int modelHandle) throws RemoteException { // Unloading a model does not require special permissions. Having a handle to the // session is sufficient. mDelegate.unloadModel(modelHandle); } @Override public IBinder startRecognition(int modelHandle, @NonNull RecognitionConfig config) throws RemoteException { enforcePermissions(); return mDelegate.startRecognition(modelHandle, config); } @Override public void stopRecognition(int modelHandle) throws RemoteException { // Stopping a model does not require special permissions. Having a handle to the // session is sufficient. mDelegate.stopRecognition(modelHandle); } @Override public void forceRecognitionEvent(int modelHandle) throws RemoteException { enforcePermissions(); mDelegate.forceRecognitionEvent(modelHandle); } @Override public void setModelParameter(int modelHandle, int modelParam, int value) throws RemoteException { enforcePermissions(); mDelegate.setModelParameter(modelHandle, modelParam, value); } @Override public int getModelParameter(int modelHandle, int modelParam) throws RemoteException { enforcePermissions(); return mDelegate.getModelParameter(modelHandle, modelParam); } @Override @Nullable public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) throws RemoteException { enforcePermissions(); return mDelegate.queryModelParameterSupport(modelHandle, modelParam); } @Override public void detach() throws RemoteException { // Detaching does not require special permissions. Having a handle to the session is // sufficient. mDelegate.detach(); } // Override toString() in order to have the delegate's ID in it. @Override public String toString() { return Objects.toString(mDelegate); } private void enforcePermissions() { enforcePermissionsForPreflight(mOriginatorIdentity); } private class CallbackWrapper implements ISoundTriggerCallback { private final ISoundTriggerCallback mDelegate; private CallbackWrapper(ISoundTriggerCallback delegate) { mDelegate = delegate; } @Override public void onRecognition(int modelHandle, RecognitionEventSys event, int captureSession) throws RemoteException { enforcePermissions("Sound trigger recognition."); mDelegate.onRecognition(modelHandle, event, captureSession); } @Override public void onPhraseRecognition(int modelHandle, PhraseRecognitionEventSys event, int captureSession) throws RemoteException { enforcePermissions("Sound trigger phrase recognition."); mDelegate.onPhraseRecognition(modelHandle, event, captureSession); } @Override public void onResourcesAvailable() throws RemoteException { mDelegate.onResourcesAvailable(); } @Override public void onModelUnloaded(int modelHandle) throws RemoteException { mDelegate.onModelUnloaded(modelHandle); } @Override public void onModuleDied() throws RemoteException { mDelegate.onModuleDied(); } @Override public IBinder asBinder() { return mDelegate.asBinder(); } // Override toString() in order to have the delegate's ID in it. @Override public String toString() { return mDelegate.toString(); } private void enforcePermissions(String reason) { if (mIsTrusted) { enforcePermissionsForPreflight(mOriginatorIdentity); } else { enforcePermissionsForDataDelivery(mOriginatorIdentity, reason); } } } } }