1 /*
2  * Copyright (C) 2020 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 
17 package com.android.server.soundtrigger_middleware;
18 
19 import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
20 import static android.Manifest.permission.RECORD_AUDIO;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.content.Context;
25 import android.content.PermissionChecker;
26 import android.media.permission.Identity;
27 import android.media.permission.IdentityContext;
28 import android.media.permission.PermissionUtil;
29 import android.media.soundtrigger.ModelParameterRange;
30 import android.media.soundtrigger.PhraseSoundModel;
31 import android.media.soundtrigger.RecognitionConfig;
32 import android.media.soundtrigger.SoundModel;
33 import android.media.soundtrigger.Status;
34 import android.media.soundtrigger_middleware.ISoundTriggerCallback;
35 import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
36 import android.media.soundtrigger_middleware.ISoundTriggerModule;
37 import android.media.soundtrigger_middleware.PhraseRecognitionEventSys;
38 import android.media.soundtrigger_middleware.RecognitionEventSys;
39 import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
40 import android.os.IBinder;
41 import android.os.RemoteException;
42 import android.os.ServiceSpecificException;
43 
44 import com.android.server.LocalServices;
45 import com.android.server.pm.permission.LegacyPermissionManagerInternal;
46 
47 import java.io.PrintWriter;
48 import java.util.Objects;
49 
50 /**
51  * This is a decorator of an {@link ISoundTriggerMiddlewareService}, which enforces permissions.
52  * <p>
53  * Every public method in this class, overriding an interface method, must follow a similar
54  * pattern:
55  * <code><pre>
56  * @Override public T method(S arg) {
57  *     // Permission check.
58  *     enforcePermissions*(...);
59  *     return mDelegate.method(arg);
60  * }
61  * </pre></code>
62  *
63  * {@hide}
64  */
65 public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddlewareInternal, Dumpable {
66     private static final String TAG = "SoundTriggerMiddlewarePermission";
67 
68     private final @NonNull ISoundTriggerMiddlewareInternal mDelegate;
69     private final @NonNull Context mContext;
70 
SoundTriggerMiddlewarePermission( @onNull ISoundTriggerMiddlewareInternal delegate, @NonNull Context context)71     public SoundTriggerMiddlewarePermission(
72             @NonNull ISoundTriggerMiddlewareInternal delegate, @NonNull Context context) {
73         mDelegate = delegate;
74         mContext = context;
75     }
76 
77     @Override
78     public @NonNull
listModules()79     SoundTriggerModuleDescriptor[] listModules() {
80         Identity identity = getIdentity();
81         enforcePermissionForPreflight(mContext, identity, CAPTURE_AUDIO_HOTWORD);
82         return mDelegate.listModules();
83     }
84 
85     @Override
86     public @NonNull
attach(int handle, @NonNull ISoundTriggerCallback callback, boolean isTrusted)87     ISoundTriggerModule attach(int handle,
88             @NonNull ISoundTriggerCallback callback, boolean isTrusted) {
89         Identity identity = getIdentity();
90         enforcePermissionsForPreflight(identity);
91         ModuleWrapper wrapper = new ModuleWrapper(identity, callback, isTrusted);
92         return wrapper.attach(mDelegate.attach(handle, wrapper.getCallbackWrapper(), isTrusted));
93     }
94 
95     // Override toString() in order to have the delegate's ID in it.
96     @Override
toString()97     public String toString() {
98         return Objects.toString(mDelegate);
99     }
100 
101     /**
102      * Get the identity context, or throws an InternalServerError if it has not been established.
103      *
104      * @return The identity.
105      */
106     private static @NonNull
getIdentity()107     Identity getIdentity() {
108         return IdentityContext.getNonNull();
109     }
110 
111     /**
112      * Throws a {@link SecurityException} if originator permanently doesn't have the given
113      * permission,
114      * or a {@link ServiceSpecificException} with a {@link Status#TEMPORARY_PERMISSION_DENIED} if
115      * originator temporarily doesn't have the right permissions to use this service.
116      */
enforcePermissionsForPreflight(@onNull Identity identity)117     private void enforcePermissionsForPreflight(@NonNull Identity identity) {
118         enforcePermissionForPreflight(mContext, identity, RECORD_AUDIO);
119         enforcePermissionForPreflight(mContext, identity, CAPTURE_AUDIO_HOTWORD);
120     }
121 
122     /**
123      * Throws a {@link SecurityException} iff the originator has permission to receive data.
124      */
enforcePermissionsForDataDelivery(@onNull Identity identity, @NonNull String reason)125     void enforcePermissionsForDataDelivery(@NonNull Identity identity, @NonNull String reason) {
126         enforceSoundTriggerRecordAudioPermissionForDataDelivery(identity, reason);
127         enforcePermissionForDataDelivery(mContext, identity, CAPTURE_AUDIO_HOTWORD,
128                 reason);
129     }
130 
131     /**
132      * Throws a {@link SecurityException} iff the given identity has given permission to receive
133      * data.
134      *
135      * @param context    A {@link Context}, used for permission checks.
136      * @param identity   The identity to check.
137      * @param permission The identifier of the permission we want to check.
138      * @param reason     The reason why we're requesting the permission, for auditing purposes.
139      */
enforcePermissionForDataDelivery(@onNull Context context, @NonNull Identity identity, @NonNull String permission, @NonNull String reason)140     private static void enforcePermissionForDataDelivery(@NonNull Context context,
141             @NonNull Identity identity,
142             @NonNull String permission, @NonNull String reason) {
143         final int status = PermissionUtil.checkPermissionForDataDelivery(context, identity,
144                 permission, reason);
145         if (status != PermissionChecker.PERMISSION_GRANTED) {
146             throw new SecurityException(
147                     String.format("Failed to obtain permission %s for identity %s", permission,
148                             ObjectPrinter.print(identity, 16)));
149         }
150     }
151 
enforceSoundTriggerRecordAudioPermissionForDataDelivery( @onNull Identity identity, @NonNull String reason)152     private static void enforceSoundTriggerRecordAudioPermissionForDataDelivery(
153             @NonNull Identity identity, @NonNull String reason) {
154         LegacyPermissionManagerInternal lpmi =
155                 LocalServices.getService(LegacyPermissionManagerInternal.class);
156         final int status = lpmi.checkSoundTriggerRecordAudioPermissionForDataDelivery(identity.uid,
157                 identity.packageName, identity.attributionTag, reason);
158         if (status != PermissionChecker.PERMISSION_GRANTED) {
159             throw new SecurityException(
160                     String.format("Failed to obtain permission RECORD_AUDIO for identity %s",
161                             ObjectPrinter.print(identity, 16)));
162         }
163     }
164 
165     /**
166      * Throws a {@link SecurityException} if originator permanently doesn't have the given
167      * permission.
168      * Soft (temporary) denials are considered OK for preflight purposes.
169      *
170      * @param context    A {@link Context}, used for permission checks.
171      * @param identity   The identity to check.
172      * @param permission The identifier of the permission we want to check.
173      */
enforcePermissionForPreflight(@onNull Context context, @NonNull Identity identity, @NonNull String permission)174     private static void enforcePermissionForPreflight(@NonNull Context context,
175             @NonNull Identity identity, @NonNull String permission) {
176         final int status = PermissionUtil.checkPermissionForPreflight(context, identity,
177                 permission);
178         switch (status) {
179             case PermissionChecker.PERMISSION_GRANTED:
180             case PermissionChecker.PERMISSION_SOFT_DENIED:
181                 return;
182             case PermissionChecker.PERMISSION_HARD_DENIED:
183                 throw new SecurityException(
184                         String.format("Failed to obtain permission %s for identity %s", permission,
185                                 ObjectPrinter.print(identity, 16)));
186             default:
187                 throw new RuntimeException("Unexpected perimission check result.");
188         }
189     }
190 
191 
192     @Override
dump(PrintWriter pw)193     public void dump(PrintWriter pw) {
194         if (mDelegate instanceof Dumpable) {
195             ((Dumpable) mDelegate).dump(pw);
196         }
197     }
198 
199     /**
200      * A wrapper around an {@link ISoundTriggerModule} implementation, to address the same aspects
201      * mentioned in {@link SoundTriggerModule} above. This class follows the same conventions.
202      */
203     private class ModuleWrapper extends ISoundTriggerModule.Stub {
204         private ISoundTriggerModule mDelegate;
205         private final @NonNull Identity mOriginatorIdentity;
206         private final @NonNull CallbackWrapper mCallbackWrapper;
207         private final boolean mIsTrusted;
208 
ModuleWrapper(@onNull Identity originatorIdentity, @NonNull ISoundTriggerCallback callback, boolean isTrusted)209         ModuleWrapper(@NonNull Identity originatorIdentity,
210                 @NonNull ISoundTriggerCallback callback,
211                 boolean isTrusted) {
212             mOriginatorIdentity = originatorIdentity;
213             mCallbackWrapper = new CallbackWrapper(callback);
214             mIsTrusted = isTrusted;
215         }
216 
attach(@onNull ISoundTriggerModule delegate)217         ModuleWrapper attach(@NonNull ISoundTriggerModule delegate) {
218             mDelegate = delegate;
219             return this;
220         }
221 
getCallbackWrapper()222         ISoundTriggerCallback getCallbackWrapper() {
223             return mCallbackWrapper;
224         }
225 
226         @Override
loadModel(@onNull SoundModel model)227         public int loadModel(@NonNull SoundModel model) throws RemoteException {
228             enforcePermissions();
229             return mDelegate.loadModel(model);
230         }
231 
232         @Override
loadPhraseModel(@onNull PhraseSoundModel model)233         public int loadPhraseModel(@NonNull PhraseSoundModel model) throws RemoteException {
234             enforcePermissions();
235             return mDelegate.loadPhraseModel(model);
236         }
237 
238         @Override
unloadModel(int modelHandle)239         public void unloadModel(int modelHandle) throws RemoteException {
240             // Unloading a model does not require special permissions. Having a handle to the
241             // session is sufficient.
242             mDelegate.unloadModel(modelHandle);
243 
244         }
245 
246         @Override
startRecognition(int modelHandle, @NonNull RecognitionConfig config)247         public IBinder startRecognition(int modelHandle, @NonNull RecognitionConfig config)
248                 throws RemoteException {
249             enforcePermissions();
250             return mDelegate.startRecognition(modelHandle, config);
251         }
252 
253         @Override
stopRecognition(int modelHandle)254         public void stopRecognition(int modelHandle) throws RemoteException {
255             // Stopping a model does not require special permissions. Having a handle to the
256             // session is sufficient.
257             mDelegate.stopRecognition(modelHandle);
258         }
259 
260         @Override
forceRecognitionEvent(int modelHandle)261         public void forceRecognitionEvent(int modelHandle) throws RemoteException {
262             enforcePermissions();
263             mDelegate.forceRecognitionEvent(modelHandle);
264         }
265 
266         @Override
setModelParameter(int modelHandle, int modelParam, int value)267         public void setModelParameter(int modelHandle, int modelParam, int value)
268                 throws RemoteException {
269             enforcePermissions();
270             mDelegate.setModelParameter(modelHandle, modelParam, value);
271         }
272 
273         @Override
getModelParameter(int modelHandle, int modelParam)274         public int getModelParameter(int modelHandle, int modelParam) throws RemoteException {
275             enforcePermissions();
276             return mDelegate.getModelParameter(modelHandle, modelParam);
277         }
278 
279         @Override
280         @Nullable
queryModelParameterSupport(int modelHandle, int modelParam)281         public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam)
282                 throws RemoteException {
283             enforcePermissions();
284             return mDelegate.queryModelParameterSupport(modelHandle,
285                     modelParam);
286         }
287 
288         @Override
detach()289         public void detach() throws RemoteException {
290             // Detaching does not require special permissions. Having a handle to the session is
291             // sufficient.
292             mDelegate.detach();
293         }
294 
295         // Override toString() in order to have the delegate's ID in it.
296         @Override
toString()297         public String toString() {
298             return Objects.toString(mDelegate);
299         }
300 
enforcePermissions()301         private void enforcePermissions() {
302             enforcePermissionsForPreflight(mOriginatorIdentity);
303         }
304 
305         private class CallbackWrapper implements ISoundTriggerCallback {
306             private final ISoundTriggerCallback mDelegate;
307 
CallbackWrapper(ISoundTriggerCallback delegate)308             private CallbackWrapper(ISoundTriggerCallback delegate) {
309                 mDelegate = delegate;
310             }
311 
312             @Override
onRecognition(int modelHandle, RecognitionEventSys event, int captureSession)313             public void onRecognition(int modelHandle, RecognitionEventSys event,
314                     int captureSession) throws RemoteException {
315                 enforcePermissions("Sound trigger recognition.");
316                 mDelegate.onRecognition(modelHandle, event, captureSession);
317             }
318 
319             @Override
onPhraseRecognition(int modelHandle, PhraseRecognitionEventSys event, int captureSession)320             public void onPhraseRecognition(int modelHandle, PhraseRecognitionEventSys event,
321                     int captureSession) throws RemoteException {
322                 enforcePermissions("Sound trigger phrase recognition.");
323                 mDelegate.onPhraseRecognition(modelHandle, event, captureSession);
324             }
325 
326             @Override
onResourcesAvailable()327             public void onResourcesAvailable() throws RemoteException {
328                 mDelegate.onResourcesAvailable();
329             }
330 
331             @Override
onModelUnloaded(int modelHandle)332             public void onModelUnloaded(int modelHandle) throws RemoteException {
333                 mDelegate.onModelUnloaded(modelHandle);
334             }
335 
336             @Override
onModuleDied()337             public void onModuleDied() throws RemoteException {
338                 mDelegate.onModuleDied();
339             }
340 
341             @Override
asBinder()342             public IBinder asBinder() {
343                 return mDelegate.asBinder();
344             }
345 
346             // Override toString() in order to have the delegate's ID in it.
347             @Override
toString()348             public String toString() {
349                 return mDelegate.toString();
350             }
351 
enforcePermissions(String reason)352             private void enforcePermissions(String reason) {
353                 if (mIsTrusted) {
354                     enforcePermissionsForPreflight(mOriginatorIdentity);
355                 } else {
356                     enforcePermissionsForDataDelivery(mOriginatorIdentity, reason);
357                 }
358             }
359         }
360     }
361 }
362