Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1a619f03 authored by Ytai Ben-Tsvi's avatar Ytai Ben-Tsvi
Browse files

Associate an originator identity to sessions

This change formalizes the permission enforcement patterns in the
sound trigger middleware layer. Every sound trigger session is
associated with an originator identity, established during attachment.
This identity is the used to authorize any operations performed on
this session, including data deliver via callbacks.

Temporarily, for b/w compatibility the existing SoundTrigger.java API
is preserved. Follow up changes will use the newer API, which requires
an identity to be provided.

Logging / dumpsys aspects have been modified to include the originator
identity associated with every call / session.

Change-Id: If83b151bd182af5a0dd98ff23dce252018de936b
Bug: 163865561
parent 4f6f70cd
Loading
Loading
Loading
Loading
+166 −18
Original line number Diff line number Diff line
@@ -16,6 +16,9 @@

package android.hardware.soundtrigger;

import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
import static android.Manifest.permission.RECORD_AUDIO;
import static android.Manifest.permission.SOUNDTRIGGER_DELEGATE_IDENTITY;
import static android.system.OsConstants.EINVAL;
import static android.system.OsConstants.ENODEV;
import static android.system.OsConstants.ENOSYS;
@@ -27,6 +30,7 @@ import static java.util.Objects.requireNonNull;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
@@ -34,9 +38,13 @@ import android.app.ActivityThread;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.media.AudioFormat;
import android.media.permission.ClearCallingIdentityContext;
import android.media.permission.Identity;
import android.media.permission.SafeCloseable;
import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
import android.media.soundtrigger_middleware.Status;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -1943,16 +1951,6 @@ public class SoundTrigger {
    public static final int SERVICE_STATE_DISABLED = 1;
    private static Object mServiceLock = new Object();
    private static ISoundTriggerMiddlewareService mService;
   /**
     * @return returns current package name.
     */
    static String getCurrentOpPackageName() {
        String packageName = ActivityThread.currentOpPackageName();
        if (packageName == null) {
            return "";
        }
        return packageName;
    }

    /**
     * Translate an exception thrown from interaction with the underlying service to an error code.
@@ -2005,23 +2003,103 @@ public class SoundTrigger {
     *         - {@link #STATUS_BAD_VALUE} if modules is null
     *         - {@link #STATUS_DEAD_OBJECT} if the binder transaction to the native service fails
     *
     * @deprecated Please use {@link #listModulesAsOriginator(ArrayList, Identity)} or
     * {@link #listModulesAsMiddleman(ArrayList, Identity, Identity)}, based on whether the
     * client is acting on behalf of its own identity or a separate identity.
     * @hide
     */
    @UnsupportedAppUsage
    public static int listModules(@NonNull ArrayList<ModuleProperties> modules) {
        // TODO(ytai): This is a temporary hack to retain prior behavior, which makes
        //  assumptions about process affinity and Binder context, namely that the binder calling ID
        //  reliably reflects the originator identity.
        Identity middlemanIdentity = new Identity();
        middlemanIdentity.packageName = ActivityThread.currentOpPackageName();

        Identity originatorIdentity = new Identity();
        originatorIdentity.pid = Binder.getCallingPid();
        originatorIdentity.uid = Binder.getCallingUid();

        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
            return listModulesAsMiddleman(modules, middlemanIdentity, originatorIdentity);
        }
    }

    /**
     * Returns a list of descriptors for all hardware modules loaded.
     * This variant is intended for use when the caller itself is the originator of the operation.
     * @param modules A ModuleProperties array where the list will be returned.
     * @param originatorIdentity The identity of the originator, which will be used for permission
     *                           purposes.
     * @return - {@link #STATUS_OK} in case of success
     *         - {@link #STATUS_ERROR} in case of unspecified error
     *         - {@link #STATUS_PERMISSION_DENIED} if the caller does not have system permission
     *         - {@link #STATUS_NO_INIT} if the native service cannot be reached
     *         - {@link #STATUS_BAD_VALUE} if modules is null
     *         - {@link #STATUS_DEAD_OBJECT} if the binder transaction to the native service fails
     *
     * @hide
     */
    @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
    public static int listModulesAsOriginator(@NonNull ArrayList<ModuleProperties> modules,
            @NonNull Identity originatorIdentity) {
        try {
            SoundTriggerModuleDescriptor[] descs = getService().listModules();
            modules.clear();
            modules.ensureCapacity(descs.length);
            for (SoundTriggerModuleDescriptor desc : descs) {
                modules.add(ConversionUtil.aidl2apiModuleDescriptor(desc));
            SoundTriggerModuleDescriptor[] descs = getService().listModulesAsOriginator(
                    originatorIdentity);
            convertDescriptorsToModuleProperties(descs, modules);
            return STATUS_OK;
        } catch (Exception e) {
            return handleException(e);
        }
    }

    /**
     * Returns a list of descriptors for all hardware modules loaded.
     * This variant is intended for use when the caller is acting on behalf of a different identity
     * for permission purposes.
     * @param modules A ModuleProperties array where the list will be returned.
     * @param middlemanIdentity The identity of the caller, acting as middleman.
     * @param originatorIdentity The identity of the originator, which will be used for permission
     *                           purposes.
     * @return - {@link #STATUS_OK} in case of success
     *         - {@link #STATUS_ERROR} in case of unspecified error
     *         - {@link #STATUS_PERMISSION_DENIED} if the caller does not have system permission
     *         - {@link #STATUS_NO_INIT} if the native service cannot be reached
     *         - {@link #STATUS_BAD_VALUE} if modules is null
     *         - {@link #STATUS_DEAD_OBJECT} if the binder transaction to the native service fails
     *
     * @hide
     */
    @RequiresPermission(SOUNDTRIGGER_DELEGATE_IDENTITY)
    public static int listModulesAsMiddleman(@NonNull ArrayList<ModuleProperties> modules,
            @NonNull Identity middlemanIdentity,
            @NonNull Identity originatorIdentity) {
        try {
            SoundTriggerModuleDescriptor[] descs = getService().listModulesAsMiddleman(
                    middlemanIdentity, originatorIdentity);
            convertDescriptorsToModuleProperties(descs, modules);
            return STATUS_OK;
        } catch (Exception e) {
            return handleException(e);
        }
    }

    /**
     * Converts an array of SoundTriggerModuleDescriptor into an (existing) ArrayList of
     * ModuleProperties.
     * @param descsIn The input descriptors.
     * @param modulesOut The output list.
     */
    private static void convertDescriptorsToModuleProperties(
            @NonNull SoundTriggerModuleDescriptor[] descsIn,
            @NonNull ArrayList<ModuleProperties> modulesOut) {
        modulesOut.clear();
        modulesOut.ensureCapacity(descsIn.length);
        for (SoundTriggerModuleDescriptor desc : descsIn) {
            modulesOut.add(ConversionUtil.aidl2apiModuleDescriptor(desc));
        }
    }

    /**
     * Get an interface on a hardware module to control sound models and recognition on
     * this module.
@@ -2031,15 +2109,85 @@ public class SoundTrigger {
     *                is OK.
     * @return a valid sound module in case of success or null in case of error.
     *
     * @deprecated Please use
     * {@link #attachModuleAsOriginator(int, StatusListener, Handler, Identity)} or
     * {@link #attachModuleAsMiddleman(int, StatusListener, Handler, Identity, Identity)}, based
     * on whether the client is acting on behalf of its own identity or a separate identity.
     * @hide
     */
    @UnsupportedAppUsage
    public static @NonNull SoundTriggerModule attachModule(int moduleId,
    public static SoundTriggerModule attachModule(int moduleId,
            @NonNull StatusListener listener,
            @Nullable Handler handler) {
        // TODO(ytai): This is a temporary hack to retain prior behavior, which makes
        //  assumptions about process affinity and Binder context, namely that the binder calling ID
        //  reliably reflects the originator identity.
        Identity middlemanIdentity = new Identity();
        middlemanIdentity.packageName = ActivityThread.currentOpPackageName();

        Identity originatorIdentity = new Identity();
        originatorIdentity.pid = Binder.getCallingPid();
        originatorIdentity.uid = Binder.getCallingUid();

        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
            return attachModuleAsMiddleman(moduleId, listener, handler, middlemanIdentity,
                    originatorIdentity);
        }
    }

    /**
     * Get an interface on a hardware module to control sound models and recognition on
     * this module.
     * This variant is intended for use when the caller is acting on behalf of a different identity
     * for permission purposes.
     * @param moduleId Sound module system identifier {@link ModuleProperties#mId}. mandatory.
     * @param listener {@link StatusListener} interface. Mandatory.
     * @param handler the Handler that will receive the callabcks. Can be null if default handler
     *                is OK.
     * @param middlemanIdentity The identity of the caller, acting as middleman.
     * @param originatorIdentity The identity of the originator, which will be used for permission
     *                           purposes.
     * @return a valid sound module in case of success or null in case of error.
     *
     * @hide
     */
    @RequiresPermission(SOUNDTRIGGER_DELEGATE_IDENTITY)
    public static SoundTriggerModule attachModuleAsMiddleman(int moduleId,
            @NonNull SoundTrigger.StatusListener listener,
            @Nullable Handler handler, Identity middlemanIdentity,
            Identity originatorIdentity) {
        Looper looper = handler != null ? handler.getLooper() : Looper.getMainLooper();
        try {
            return new SoundTriggerModule(getService(), moduleId, listener, looper,
                    middlemanIdentity, originatorIdentity);
        } catch (Exception e) {
            Log.e(TAG, "", e);
            return null;
        }
    }

    /**
     * Get an interface on a hardware module to control sound models and recognition on
     * this module.
     * This variant is intended for use when the caller itself is the originator of the operation.
     * @param moduleId Sound module system identifier {@link ModuleProperties#mId}. mandatory.
     * @param listener {@link StatusListener} interface. Mandatory.
     * @param handler the Handler that will receive the callabcks. Can be null if default handler
     *                is OK.
     * @param originatorIdentity The identity of the originator, which will be used for permission
     *                           purposes.
     * @return a valid sound module in case of success or null in case of error.
     *
     * @hide
     */
    @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
    public static SoundTriggerModule attachModuleAsOriginator(int moduleId,
            @NonNull SoundTrigger.StatusListener listener,
            @Nullable Handler handler, @NonNull Identity originatorIdentity) {
        Looper looper = handler != null ? handler.getLooper() : Looper.getMainLooper();
        try {
            return new SoundTriggerModule(getService(), moduleId, listener, looper);
            return new SoundTriggerModule(getService(), moduleId, listener, looper,
                    originatorIdentity);
        } catch (Exception e) {
            Log.e(TAG, "", e);
            return null;
+32 −2
Original line number Diff line number Diff line
@@ -19,6 +19,9 @@ package android.hardware.soundtrigger;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.media.permission.ClearCallingIdentityContext;
import android.media.permission.Identity;
import android.media.permission.SafeCloseable;
import android.media.soundtrigger_middleware.ISoundTriggerCallback;
import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
import android.media.soundtrigger_middleware.ISoundTriggerModule;
@@ -50,12 +53,39 @@ public class SoundTriggerModule {
    private EventHandlerDelegate mEventHandlerDelegate;
    private ISoundTriggerModule mService;

    /**
     * This variant is intended for use when the caller is acting an originator, rather than on
     * behalf of a different entity, as far as authorization goes.
     */
    SoundTriggerModule(@NonNull ISoundTriggerMiddlewareService service,
            int moduleId, @NonNull SoundTrigger.StatusListener listener, @NonNull Looper looper)
            int moduleId, @NonNull SoundTrigger.StatusListener listener, @NonNull Looper looper,
            @NonNull Identity originatorIdentity)
            throws RemoteException {
        mId = moduleId;
        mEventHandlerDelegate = new EventHandlerDelegate(listener, looper);
        mService = service.attach(moduleId, mEventHandlerDelegate);

        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
            mService = service.attachAsOriginator(moduleId, originatorIdentity,
                    mEventHandlerDelegate);
        }
        mService.asBinder().linkToDeath(mEventHandlerDelegate, 0);
    }

    /**
     * This variant is intended for use when the caller is acting as a middleman, i.e. on behalf of
     * a different entity, as far as authorization goes.
     */
    SoundTriggerModule(@NonNull ISoundTriggerMiddlewareService service,
            int moduleId, @NonNull SoundTrigger.StatusListener listener, @NonNull Looper looper,
            @NonNull Identity middlemanIdentity, @NonNull Identity originatorIdentity)
            throws RemoteException {
        mId = moduleId;
        mEventHandlerDelegate = new EventHandlerDelegate(listener, looper);

        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
            mService = service.attachAsMiddleman(moduleId, middlemanIdentity, originatorIdentity,
                    mEventHandlerDelegate);
        }
        mService.asBinder().linkToDeath(mEventHandlerDelegate, 0);
    }

+9 −0
Original line number Diff line number Diff line
@@ -3972,6 +3972,15 @@
    <permission android:name="android.permission.CAPTURE_AUDIO_HOTWORD"
        android:protectionLevel="signature|privileged" />

    <!-- Puts an application in the chain of trust for sound trigger
         operations. Being in the chain of trust allows an application to
         delegate an identity of a separate entity to the sound trigger system
         and vouch for the authenticity of this identity.
         <p>Not for use by third-party applications.</p>
         @hide -->
    <permission android:name="android.permission.SOUNDTRIGGER_DELEGATE_IDENTITY"
        android:protectionLevel="signature|privileged" />

    <!-- @SystemApi Allows an application to modify audio routing and override policy decisions.
         <p>Not for use by third-party applications.</p>
         @hide -->
+23 −1
Original line number Diff line number Diff line
@@ -22,6 +22,25 @@ aidl_interface {
    },
}

aidl_interface {
    name: "media_permission-aidl",
    unstable: true,
    local_include_dir: "java",
    srcs: [
        "java/android/media/permission/Identity.aidl",
    ],
    backend:
    {
        cpp: {
            enabled: true,
        },
        java: {
            // Already generated as part of the entire media java library.
            enabled: false,
        },
    },
}

aidl_interface {
    name: "soundtrigger_middleware-aidl",
    unstable: true,
@@ -61,5 +80,8 @@ aidl_interface {
            enabled: false,
        },
    },
    imports: [ "audio_common-aidl" ],
    imports: [
        "audio_common-aidl",
        "media_permission-aidl",
    ],
}
+58 −0
Original line number Diff line number Diff line
/*
 * 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 android.media.permission;

import android.annotation.NonNull;
import android.os.Binder;

/**
 * An RAII-style object, used to establish a scope in which the binder calling identity is cleared.
 *
 * <p>
 * Intended usage:
 * <pre>
 * void caller() {
 *   try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
 *       // Within this scope the binder calling identity is cleared.
 *       ...
 *   }
 *   // Outside the scope the calling identity is restored to its prior state.
 * </pre>
 *
 * @hide
 */
public class ClearCallingIdentityContext implements SafeCloseable {
    private final long mRestoreKey;

    /**
     * Creates a new instance.
     * @return A {@link SafeCloseable}, intended to be used in a try-with-resource block.
     */
    public static @NonNull
    SafeCloseable create() {
        return new ClearCallingIdentityContext();
    }

    private ClearCallingIdentityContext() {
        mRestoreKey = Binder.clearCallingIdentity();
    }

    @Override
    public void close() {
        Binder.restoreCallingIdentity(mRestoreKey);
    }
}
Loading