/* * 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:
*
*
* {@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);
}
}
}
}
}
* @Override public T method(S arg) {
* // Permission check.
* enforcePermissions*(...);
* return mDelegate.method(arg);
* }
*