/* * Copyright (C) 2013 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; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.UUID; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StringDef; import android.annotation.SystemApi; import android.app.ActivityThread; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Parcel; import android.util.Log; /** * MediaDrm can be used to obtain keys for decrypting protected media streams, in * conjunction with {@link android.media.MediaCrypto}. The MediaDrm APIs * are designed to support the ISO/IEC 23001-7: Common Encryption standard, but * may also be used to implement other encryption schemes. *

* Encrypted content is prepared using an encryption server and stored in a content * library. The encrypted content is streamed or downloaded from the content library to * client devices via content servers. Licenses to view the content are obtained from * a License Server. *

*

MediaDrm Overview diagram

*

* Keys are requested from the license server using a key request. The key * response is delivered to the client app, which provides the response to the * MediaDrm API. *

* A Provisioning server may be required to distribute device-unique credentials to * the devices. *

* Enforcing requirements related to the number of devices that may play content * simultaneously can be performed either through key renewal or using the secure * stop methods. *

* The following sequence diagram shows the interactions between the objects * involved while playing back encrypted content: *

*

MediaDrm Overview diagram

*

* The app first constructs {@link android.media.MediaExtractor} and * {@link android.media.MediaCodec} objects. It accesses the DRM-scheme-identifying UUID, * typically from metadata in the content, and uses this UUID to construct an instance * of a MediaDrm object that is able to support the DRM scheme required by the content. * Crypto schemes are assigned 16 byte UUIDs. The method {@link #isCryptoSchemeSupported} * can be used to query if a given scheme is supported on the device. *

* The app calls {@link #openSession} to generate a sessionId that will uniquely identify * the session in subsequent interactions. The app next uses the MediaDrm object to * obtain a key request message and send it to the license server, then provide * the server's response to the MediaDrm object. *

* Once the app has a sessionId, it can construct a MediaCrypto object from the UUID and * sessionId. The MediaCrypto object is registered with the MediaCodec in the * {@link MediaCodec.#configure} method to enable the codec to decrypt content. *

* When the app has constructed {@link android.media.MediaExtractor}, * {@link android.media.MediaCodec} and {@link android.media.MediaCrypto} objects, * it proceeds to pull samples from the extractor and queue them into the decoder. For * encrypted content, the samples returned from the extractor remain encrypted, they * are only decrypted when the samples are delivered to the decoder. *

* MediaDrm methods throw {@link android.media.MediaDrm.MediaDrmStateException} * when a method is called on a MediaDrm object that has had an unrecoverable failure * in the DRM plugin or security hardware. * {@link android.media.MediaDrm.MediaDrmStateException} extends * {@link java.lang.IllegalStateException} with the addition of a developer-readable * diagnostic information string associated with the exception. *

* In the event of a mediaserver process crash or restart while a MediaDrm object * is active, MediaDrm methods may throw {@link android.media.MediaDrmResetException}. * To recover, the app must release the MediaDrm object, then create and initialize * a new one. *

* As {@link android.media.MediaDrmResetException} and * {@link android.media.MediaDrm.MediaDrmStateException} both extend * {@link java.lang.IllegalStateException}, they should be in an earlier catch() * block than {@link java.lang.IllegalStateException} if handled separately. *

* *

Callbacks

*

Applications should register for informational events in order * to be informed of key state updates during playback or streaming. * Registration for these events is done via a call to * {@link #setOnEventListener}. In order to receive the respective * callback associated with this listener, applications are required to create * MediaDrm objects on a thread with its own Looper running (main UI * thread by default has a Looper running). */ public final class MediaDrm { private static final String TAG = "MediaDrm"; private static final String PERMISSION = android.Manifest.permission.ACCESS_DRM_CERTIFICATES; private EventHandler mEventHandler; private EventHandler mOnKeyStatusChangeEventHandler; private EventHandler mOnExpirationUpdateEventHandler; private OnEventListener mOnEventListener; private OnKeyStatusChangeListener mOnKeyStatusChangeListener; private OnExpirationUpdateListener mOnExpirationUpdateListener; private long mNativeContext; /** * Specify no certificate type * * @hide - not part of the public API at this time */ public static final int CERTIFICATE_TYPE_NONE = 0; /** * Specify X.509 certificate type * * @hide - not part of the public API at this time */ public static final int CERTIFICATE_TYPE_X509 = 1; /** @hide */ @IntDef({ CERTIFICATE_TYPE_NONE, CERTIFICATE_TYPE_X509, }) @Retention(RetentionPolicy.SOURCE) public @interface CertificateType {} /** * Query if the given scheme identified by its UUID is supported on * this device. * @param uuid The UUID of the crypto scheme. */ public static final boolean isCryptoSchemeSupported(@NonNull UUID uuid) { return isCryptoSchemeSupportedNative(getByteArrayFromUUID(uuid), null); } /** * Query if the given scheme identified by its UUID is supported on * this device, and whether the drm plugin is able to handle the * media container format specified by mimeType. * @param uuid The UUID of the crypto scheme. * @param mimeType The MIME type of the media container, e.g. "video/mp4" * or "video/webm" */ public static final boolean isCryptoSchemeSupported( @NonNull UUID uuid, @NonNull String mimeType) { return isCryptoSchemeSupportedNative(getByteArrayFromUUID(uuid), mimeType); } private static final byte[] getByteArrayFromUUID(@NonNull UUID uuid) { long msb = uuid.getMostSignificantBits(); long lsb = uuid.getLeastSignificantBits(); byte[] uuidBytes = new byte[16]; for (int i = 0; i < 8; ++i) { uuidBytes[i] = (byte)(msb >>> (8 * (7 - i))); uuidBytes[8 + i] = (byte)(lsb >>> (8 * (7 - i))); } return uuidBytes; } private static final native boolean isCryptoSchemeSupportedNative( @NonNull byte[] uuid, @Nullable String mimeType); /** * Instantiate a MediaDrm object * * @param uuid The UUID of the crypto scheme. * * @throws UnsupportedSchemeException if the device does not support the * specified scheme UUID */ public MediaDrm(@NonNull UUID uuid) throws UnsupportedSchemeException { Looper looper; if ((looper = Looper.myLooper()) != null) { mEventHandler = new EventHandler(this, looper); } else if ((looper = Looper.getMainLooper()) != null) { mEventHandler = new EventHandler(this, looper); } else { mEventHandler = null; } /* Native setup requires a weak reference to our object. * It's easier to create it here than in C++. */ native_setup(new WeakReference(this), getByteArrayFromUUID(uuid), ActivityThread.currentOpPackageName()); } /** * Thrown when an unrecoverable failure occurs during a MediaDrm operation. * Extends java.lang.IllegalStateException with the addition of an error * code that may be useful in diagnosing the failure. */ public static final class MediaDrmStateException extends java.lang.IllegalStateException { private final int mErrorCode; private final String mDiagnosticInfo; /** * @hide */ public MediaDrmStateException(int errorCode, @Nullable String detailMessage) { super(detailMessage); mErrorCode = errorCode; // TODO get this from DRM session final String sign = errorCode < 0 ? "neg_" : ""; mDiagnosticInfo = "android.media.MediaDrm.error_" + sign + Math.abs(errorCode); } /** * Retrieve the associated error code * * @hide */ public int getErrorCode() { return mErrorCode; } /** * Retrieve a developer-readable diagnostic information string * associated with the exception. Do not show this to end-users, * since this string will not be localized or generally comprehensible * to end-users. */ @NonNull public String getDiagnosticInfo() { return mDiagnosticInfo; } } /** * Register a callback to be invoked when a session expiration update * occurs. The app's OnExpirationUpdateListener will be notified * when the expiration time of the keys in the session have changed. * @param listener the callback that will be run, or {@code null} to unregister the * previously registered callback. * @param handler the handler on which the listener should be invoked, or * {@code null} if the listener should be invoked on the calling thread's looper. */ public void setOnExpirationUpdateListener( @Nullable OnExpirationUpdateListener listener, @Nullable Handler handler) { if (listener != null) { Looper looper = handler != null ? handler.getLooper() : Looper.myLooper(); if (looper != null) { if (mEventHandler == null || mEventHandler.getLooper() != looper) { mEventHandler = new EventHandler(this, looper); } } } mOnExpirationUpdateListener = listener; } /** * Interface definition for a callback to be invoked when a drm session * expiration update occurs */ public interface OnExpirationUpdateListener { /** * Called when a session expiration update occurs, to inform the app * about the change in expiration time * * @param md the MediaDrm object on which the event occurred * @param sessionId the DRM session ID on which the event occurred * @param expirationTime the new expiration time for the keys in the session. * The time is in milliseconds, relative to the Unix epoch. A time of * 0 indicates that the keys never expire. */ void onExpirationUpdate( @NonNull MediaDrm md, @NonNull byte[] sessionId, long expirationTime); } /** * Register a callback to be invoked when the state of keys in a session * change, e.g. when a license update occurs or when a license expires. * * @param listener the callback that will be run when key status changes, or * {@code null} to unregister the previously registered callback. * @param handler the handler on which the listener should be invoked, or * null if the listener should be invoked on the calling thread's looper. */ public void setOnKeyStatusChangeListener( @Nullable OnKeyStatusChangeListener listener, @Nullable Handler handler) { if (listener != null) { Looper looper = handler != null ? handler.getLooper() : Looper.myLooper(); if (looper != null) { if (mEventHandler == null || mEventHandler.getLooper() != looper) { mEventHandler = new EventHandler(this, looper); } } } mOnKeyStatusChangeListener = listener; } /** * Interface definition for a callback to be invoked when the keys in a drm * session change states. */ public interface OnKeyStatusChangeListener { /** * Called when the keys in a session change status, such as when the license * is renewed or expires. * * @param md the MediaDrm object on which the event occurred * @param sessionId the DRM session ID on which the event occurred * @param keyInformation a list of {@link MediaDrm.KeyStatus} * instances indicating the status for each key in the session * @param hasNewUsableKey indicates if a key has been added that is usable, * which may trigger an attempt to resume playback on the media stream * if it is currently blocked waiting for a key. */ void onKeyStatusChange( @NonNull MediaDrm md, @NonNull byte[] sessionId, @NonNull List keyInformation, boolean hasNewUsableKey); } /** * Defines the status of a key. * A KeyStatus for each key in a session is provided to the * {@link OnKeyStatusChangeListener#onKeyStatusChange} * listener. */ public static final class KeyStatus { private final byte[] mKeyId; private final int mStatusCode; /** * The key is currently usable to decrypt media data */ public static final int STATUS_USABLE = 0; /** * The key is no longer usable to decrypt media data because its * expiration time has passed. */ public static final int STATUS_EXPIRED = 1; /** * The key is not currently usable to decrypt media data because its * output requirements cannot currently be met. */ public static final int STATUS_OUTPUT_NOT_ALLOWED = 2; /** * The status of the key is not yet known and is being determined. * The status will be updated with the actual status when it has * been determined. */ public static final int STATUS_PENDING = 3; /** * The key is not currently usable to decrypt media data because of an * internal error in processing unrelated to input parameters. This error * is not actionable by an app. */ public static final int STATUS_INTERNAL_ERROR = 4; /** @hide */ @IntDef({ STATUS_USABLE, STATUS_EXPIRED, STATUS_OUTPUT_NOT_ALLOWED, STATUS_PENDING, STATUS_INTERNAL_ERROR, }) @Retention(RetentionPolicy.SOURCE) public @interface KeyStatusCode {} KeyStatus(@NonNull byte[] keyId, @KeyStatusCode int statusCode) { mKeyId = keyId; mStatusCode = statusCode; } /** * Returns the status code for the key * @return one of {@link #STATUS_USABLE}, {@link #STATUS_EXPIRED}, * {@link #STATUS_OUTPUT_NOT_ALLOWED}, {@link #STATUS_PENDING} * or {@link #STATUS_INTERNAL_ERROR}. */ @KeyStatusCode public int getStatusCode() { return mStatusCode; } /** * Returns the id for the key */ @NonNull public byte[] getKeyId() { return mKeyId; } } /** * Register a callback to be invoked when an event occurs * * @param listener the callback that will be run. Use {@code null} to * stop receiving event callbacks. */ public void setOnEventListener(@Nullable OnEventListener listener) { mOnEventListener = listener; } /** * Interface definition for a callback to be invoked when a drm event * occurs */ public interface OnEventListener { /** * Called when an event occurs that requires the app to be notified * * @param md the MediaDrm object on which the event occurred * @param sessionId the DRM session ID on which the event occurred, * or {@code null} if there is no session ID associated with the event. * @param event indicates the event type * @param extra an secondary error code * @param data optional byte array of data that may be associated with the event */ void onEvent( @NonNull MediaDrm md, @Nullable byte[] sessionId, @DrmEvent int event, int extra, @Nullable byte[] data); } /** * This event type indicates that the app needs to request a certificate from * the provisioning server. The request message data is obtained using * {@link #getProvisionRequest} * * @deprecated Handle provisioning via {@link android.media.NotProvisionedException} * instead. */ public static final int EVENT_PROVISION_REQUIRED = 1; /** * This event type indicates that the app needs to request keys from a license * server. The request message data is obtained using {@link #getKeyRequest}. */ public static final int EVENT_KEY_REQUIRED = 2; /** * This event type indicates that the licensed usage duration for keys in a session * has expired. The keys are no longer valid. * @deprecated Use {@link OnKeyStatusChangeListener#onKeyStatusChange} * and check for {@link MediaDrm.KeyStatus#STATUS_EXPIRED} in the {@link MediaDrm.KeyStatus} * instead. */ public static final int EVENT_KEY_EXPIRED = 3; /** * This event may indicate some specific vendor-defined condition, see your * DRM provider documentation for details */ public static final int EVENT_VENDOR_DEFINED = 4; /** * This event indicates that a session opened by the app has been reclaimed by the resource * manager. */ public static final int EVENT_SESSION_RECLAIMED = 5; /** @hide */ @IntDef({ EVENT_PROVISION_REQUIRED, EVENT_KEY_REQUIRED, EVENT_KEY_EXPIRED, EVENT_VENDOR_DEFINED, EVENT_SESSION_RECLAIMED, }) @Retention(RetentionPolicy.SOURCE) public @interface DrmEvent {} private static final int DRM_EVENT = 200; private static final int EXPIRATION_UPDATE = 201; private static final int KEY_STATUS_CHANGE = 202; private class EventHandler extends Handler { private MediaDrm mMediaDrm; public EventHandler(@NonNull MediaDrm md, @NonNull Looper looper) { super(looper); mMediaDrm = md; } @Override public void handleMessage(@NonNull Message msg) { if (mMediaDrm.mNativeContext == 0) { Log.w(TAG, "MediaDrm went away with unhandled events"); return; } switch(msg.what) { case DRM_EVENT: if (mOnEventListener != null) { if (msg.obj != null && msg.obj instanceof Parcel) { Parcel parcel = (Parcel)msg.obj; byte[] sessionId = parcel.createByteArray(); if (sessionId.length == 0) { sessionId = null; } byte[] data = parcel.createByteArray(); if (data.length == 0) { data = null; } Log.i(TAG, "Drm event (" + msg.arg1 + "," + msg.arg2 + ")"); mOnEventListener.onEvent(mMediaDrm, sessionId, msg.arg1, msg.arg2, data); } } return; case KEY_STATUS_CHANGE: if (mOnKeyStatusChangeListener != null) { if (msg.obj != null && msg.obj instanceof Parcel) { Parcel parcel = (Parcel)msg.obj; byte[] sessionId = parcel.createByteArray(); if (sessionId.length > 0) { List keyStatusList = keyStatusListFromParcel(parcel); boolean hasNewUsableKey = (parcel.readInt() != 0); Log.i(TAG, "Drm key status changed"); mOnKeyStatusChangeListener.onKeyStatusChange(mMediaDrm, sessionId, keyStatusList, hasNewUsableKey); } } } return; case EXPIRATION_UPDATE: if (mOnExpirationUpdateListener != null) { if (msg.obj != null && msg.obj instanceof Parcel) { Parcel parcel = (Parcel)msg.obj; byte[] sessionId = parcel.createByteArray(); if (sessionId.length > 0) { long expirationTime = parcel.readLong(); Log.i(TAG, "Drm key expiration update: " + expirationTime); mOnExpirationUpdateListener.onExpirationUpdate(mMediaDrm, sessionId, expirationTime); } } } return; default: Log.e(TAG, "Unknown message type " + msg.what); return; } } } /** * Parse a list of KeyStatus objects from an event parcel */ @NonNull private List keyStatusListFromParcel(@NonNull Parcel parcel) { int nelems = parcel.readInt(); List keyStatusList = new ArrayList(nelems); while (nelems-- > 0) { byte[] keyId = parcel.createByteArray(); int keyStatusCode = parcel.readInt(); keyStatusList.add(new KeyStatus(keyId, keyStatusCode)); } return keyStatusList; } /** * This method is called from native code when an event occurs. This method * just uses the EventHandler system to post the event back to the main app thread. * We use a weak reference to the original MediaPlayer object so that the native * code is safe from the object disappearing from underneath it. (This is * the cookie passed to native_setup().) */ private static void postEventFromNative(@NonNull Object mediadrm_ref, int what, int eventType, int extra, @Nullable Object obj) { MediaDrm md = (MediaDrm)((WeakReference)mediadrm_ref).get(); if (md == null) { return; } if (md.mEventHandler != null) { Message m = md.mEventHandler.obtainMessage(what, eventType, extra, obj); md.mEventHandler.sendMessage(m); } } /** * Open a new session with the MediaDrm object. A session ID is returned. * * @throws NotProvisionedException if provisioning is needed * @throws ResourceBusyException if required resources are in use */ @NonNull public native byte[] openSession() throws NotProvisionedException, ResourceBusyException; /** * Close a session on the MediaDrm object that was previously opened * with {@link #openSession}. */ public native void closeSession(@NonNull byte[] sessionId); /** * This key request type species that the keys will be for online use, they will * not be saved to the device for subsequent use when the device is not connected * to a network. */ public static final int KEY_TYPE_STREAMING = 1; /** * This key request type specifies that the keys will be for offline use, they * will be saved to the device for use when the device is not connected to a network. */ public static final int KEY_TYPE_OFFLINE = 2; /** * This key request type specifies that previously saved offline keys should be released. */ public static final int KEY_TYPE_RELEASE = 3; /** @hide */ @IntDef({ KEY_TYPE_STREAMING, KEY_TYPE_OFFLINE, KEY_TYPE_RELEASE, }) @Retention(RetentionPolicy.SOURCE) public @interface KeyType {} /** * Contains the opaque data an app uses to request keys from a license server */ public static final class KeyRequest { private byte[] mData; private String mDefaultUrl; private int mRequestType; /** * Key request type is initial license request */ public static final int REQUEST_TYPE_INITIAL = 0; /** * Key request type is license renewal */ public static final int REQUEST_TYPE_RENEWAL = 1; /** * Key request type is license release */ public static final int REQUEST_TYPE_RELEASE = 2; /** @hide */ @IntDef({ REQUEST_TYPE_INITIAL, REQUEST_TYPE_RENEWAL, REQUEST_TYPE_RELEASE, }) @Retention(RetentionPolicy.SOURCE) public @interface RequestType {} KeyRequest() {} /** * Get the opaque message data */ @NonNull public byte[] getData() { if (mData == null) { // this should never happen as mData is initialized in // JNI after construction of the KeyRequest object. The check // is needed here to guarantee @NonNull annotation. throw new RuntimeException("KeyRequest is not initialized"); } return mData; } /** * Get the default URL to use when sending the key request message to a * server, if known. The app may prefer to use a different license * server URL from other sources. * This method returns an empty string if the default URL is not known. */ @NonNull public String getDefaultUrl() { if (mDefaultUrl == null) { // this should never happen as mDefaultUrl is initialized in // JNI after construction of the KeyRequest object. The check // is needed here to guarantee @NonNull annotation. throw new RuntimeException("KeyRequest is not initialized"); } return mDefaultUrl; } /** * Get the type of the request * @return one of {@link #REQUEST_TYPE_INITIAL}, * {@link #REQUEST_TYPE_RENEWAL} or {@link #REQUEST_TYPE_RELEASE} */ @RequestType public int getRequestType() { return mRequestType; } }; /** * A key request/response exchange occurs between the app and a license server * to obtain or release keys used to decrypt encrypted content. *

* getKeyRequest() is used to obtain an opaque key request byte array that is * delivered to the license server. The opaque key request byte array is returned * in KeyRequest.data. The recommended URL to deliver the key request to is * returned in KeyRequest.defaultUrl. *

* After the app has received the key request response from the server, * it should deliver to the response to the DRM engine plugin using the method * {@link #provideKeyResponse}. * * @param scope may be a sessionId or a keySetId, depending on the specified keyType. * When the keyType is KEY_TYPE_STREAMING or KEY_TYPE_OFFLINE, * scope should be set to the sessionId the keys will be provided to. When the keyType * is KEY_TYPE_RELEASE, scope should be set to the keySetId of the keys * being released. Releasing keys from a device invalidates them for all sessions. * @param init container-specific data, its meaning is interpreted based on the * mime type provided in the mimeType parameter. It could contain, for example, * the content ID, key ID or other data obtained from the content metadata that is * required in generating the key request. May be null when keyType is * KEY_TYPE_RELEASE or if the request is a renewal, i.e. not the first key * request for the session. * @param mimeType identifies the mime type of the content. May be null if the * keyType is KEY_TYPE_RELEASE or if the request is a renewal, i.e. not the * first key request for the session. * @param keyType specifes the type of the request. The request may be to acquire * keys for streaming or offline content, or to release previously acquired * keys, which are identified by a keySetId. * @param optionalParameters are included in the key request message to * allow a client application to provide additional message parameters to the server. * This may be {@code null} if no additional parameters are to be sent. * @throws NotProvisionedException if reprovisioning is needed, due to a * problem with the certifcate */ @NonNull public native KeyRequest getKeyRequest( @NonNull byte[] scope, @Nullable byte[] init, @Nullable String mimeType, @KeyType int keyType, @Nullable HashMap optionalParameters) throws NotProvisionedException; /** * A key response is received from the license server by the app, then it is * provided to the DRM engine plugin using provideKeyResponse. When the * response is for an offline key request, a keySetId is returned that can be * used to later restore the keys to a new session with the method * {@link #restoreKeys}. * When the response is for a streaming or release request, an empty byte array * is returned. * * @param scope may be a sessionId or keySetId depending on the type of the * response. Scope should be set to the sessionId when the response is for either * streaming or offline key requests. Scope should be set to the keySetId when * the response is for a release request. * @param response the byte array response from the server * @return If the response is for an offline request, the keySetId for the offline * keys will be returned. If the response is for a streaming or release request * an empty byte array will be returned. * * @throws NotProvisionedException if the response indicates that * reprovisioning is required * @throws DeniedByServerException if the response indicates that the * server rejected the request */ @Nullable public native byte[] provideKeyResponse( @NonNull byte[] scope, @NonNull byte[] response) throws NotProvisionedException, DeniedByServerException; /** * Restore persisted offline keys into a new session. keySetId identifies the * keys to load, obtained from a prior call to {@link #provideKeyResponse}. * * @param sessionId the session ID for the DRM session * @param keySetId identifies the saved key set to restore */ public native void restoreKeys(@NonNull byte[] sessionId, @NonNull byte[] keySetId); /** * Remove the current keys from a session. * * @param sessionId the session ID for the DRM session */ public native void removeKeys(@NonNull byte[] sessionId); /** * Request an informative description of the key status for the session. The status is * in the form of {name, value} pairs. Since DRM license policies vary by vendor, * the specific status field names are determined by each DRM vendor. Refer to your * DRM provider documentation for definitions of the field names for a particular * DRM engine plugin. * * @param sessionId the session ID for the DRM session */ @NonNull public native HashMap queryKeyStatus(@NonNull byte[] sessionId); /** * Contains the opaque data an app uses to request a certificate from a provisioning * server */ public static final class ProvisionRequest { ProvisionRequest() {} /** * Get the opaque message data */ @NonNull public byte[] getData() { if (mData == null) { // this should never happen as mData is initialized in // JNI after construction of the KeyRequest object. The check // is needed here to guarantee @NonNull annotation. throw new RuntimeException("ProvisionRequest is not initialized"); } return mData; } /** * Get the default URL to use when sending the provision request * message to a server, if known. The app may prefer to use a different * provisioning server URL obtained from other sources. * This method returns an empty string if the default URL is not known. */ @NonNull public String getDefaultUrl() { if (mDefaultUrl == null) { // this should never happen as mDefaultUrl is initialized in // JNI after construction of the ProvisionRequest object. The check // is needed here to guarantee @NonNull annotation. throw new RuntimeException("ProvisionRequest is not initialized"); } return mDefaultUrl; } private byte[] mData; private String mDefaultUrl; } /** * A provision request/response exchange occurs between the app and a provisioning * server to retrieve a device certificate. If provisionining is required, the * EVENT_PROVISION_REQUIRED event will be sent to the event handler. * getProvisionRequest is used to obtain the opaque provision request byte array that * should be delivered to the provisioning server. The provision request byte array * is returned in ProvisionRequest.data. The recommended URL to deliver the provision * request to is returned in ProvisionRequest.defaultUrl. */ @NonNull public ProvisionRequest getProvisionRequest() { return getProvisionRequestNative(CERTIFICATE_TYPE_NONE, ""); } @NonNull private native ProvisionRequest getProvisionRequestNative(int certType, @NonNull String certAuthority); /** * After a provision response is received by the app, it is provided to the DRM * engine plugin using this method. * * @param response the opaque provisioning response byte array to provide to the * DRM engine plugin. * * @throws DeniedByServerException if the response indicates that the * server rejected the request */ public void provideProvisionResponse(@NonNull byte[] response) throws DeniedByServerException { provideProvisionResponseNative(response); } @NonNull /* could there be a valid response with 0-sized certificate or key? */ private native Certificate provideProvisionResponseNative(@NonNull byte[] response) throws DeniedByServerException; /** * A means of enforcing limits on the number of concurrent streams per subscriber * across devices is provided via SecureStop. This is achieved by securely * monitoring the lifetime of sessions. *

* Information from the server related to the current playback session is written * to persistent storage on the device when each MediaCrypto object is created. *

* In the normal case, playback will be completed, the session destroyed and the * Secure Stops will be queried. The app queries secure stops and forwards the * secure stop message to the server which verifies the signature and notifies the * server side database that the session destruction has been confirmed. The persisted * record on the client is only removed after positive confirmation that the server * received the message using releaseSecureStops(). */ @NonNull public native List getSecureStops(); /** * Access secure stop by secure stop ID. * * @param ssid - The secure stop ID provided by the license server. */ @NonNull public native byte[] getSecureStop(@NonNull byte[] ssid); /** * Process the SecureStop server response message ssRelease. After authenticating * the message, remove the SecureStops identified in the response. * * @param ssRelease the server response indicating which secure stops to release */ public native void releaseSecureStops(@NonNull byte[] ssRelease); /** * Remove all secure stops without requiring interaction with the server. */ public native void releaseAllSecureStops(); /** * String property name: identifies the maker of the DRM engine plugin */ public static final String PROPERTY_VENDOR = "vendor"; /** * String property name: identifies the version of the DRM engine plugin */ public static final String PROPERTY_VERSION = "version"; /** * String property name: describes the DRM engine plugin */ public static final String PROPERTY_DESCRIPTION = "description"; /** * String property name: a comma-separated list of cipher and mac algorithms * supported by CryptoSession. The list may be empty if the DRM engine * plugin does not support CryptoSession operations. */ public static final String PROPERTY_ALGORITHMS = "algorithms"; /** @hide */ @StringDef({ PROPERTY_VENDOR, PROPERTY_VERSION, PROPERTY_DESCRIPTION, PROPERTY_ALGORITHMS, }) @Retention(RetentionPolicy.SOURCE) public @interface StringProperty {} /** * Read a DRM engine plugin String property value, given the property name string. *

* Standard fields names are: * {@link #PROPERTY_VENDOR}, {@link #PROPERTY_VERSION}, * {@link #PROPERTY_DESCRIPTION}, {@link #PROPERTY_ALGORITHMS} */ /* FIXME this throws IllegalStateException for invalid property names */ @NonNull public native String getPropertyString(@NonNull @StringProperty String propertyName); /** * Byte array property name: the device unique identifier is established during * device provisioning and provides a means of uniquely identifying each device. */ /* FIXME this throws IllegalStateException for invalid property names */ public static final String PROPERTY_DEVICE_UNIQUE_ID = "deviceUniqueId"; /** @hide */ @StringDef({ PROPERTY_DEVICE_UNIQUE_ID, }) @Retention(RetentionPolicy.SOURCE) public @interface ArrayProperty {} /** * Read a DRM engine plugin byte array property value, given the property name string. *

* Standard fields names are {@link #PROPERTY_DEVICE_UNIQUE_ID} */ @NonNull public native byte[] getPropertyByteArray(@ArrayProperty String propertyName); /** * Set a DRM engine plugin String property value. */ public native void setPropertyString( String propertyName, @NonNull String value); /** * Set a DRM engine plugin byte array property value. */ public native void setPropertyByteArray( String propertyName, @NonNull byte[] value); private static final native void setCipherAlgorithmNative( @NonNull MediaDrm drm, @NonNull byte[] sessionId, @NonNull String algorithm); private static final native void setMacAlgorithmNative( @NonNull MediaDrm drm, @NonNull byte[] sessionId, @NonNull String algorithm); @NonNull private static final native byte[] encryptNative( @NonNull MediaDrm drm, @NonNull byte[] sessionId, @NonNull byte[] keyId, @NonNull byte[] input, @NonNull byte[] iv); @NonNull private static final native byte[] decryptNative( @NonNull MediaDrm drm, @NonNull byte[] sessionId, @NonNull byte[] keyId, @NonNull byte[] input, @NonNull byte[] iv); @NonNull private static final native byte[] signNative( @NonNull MediaDrm drm, @NonNull byte[] sessionId, @NonNull byte[] keyId, @NonNull byte[] message); private static final native boolean verifyNative( @NonNull MediaDrm drm, @NonNull byte[] sessionId, @NonNull byte[] keyId, @NonNull byte[] message, @NonNull byte[] signature); /** * In addition to supporting decryption of DASH Common Encrypted Media, the * MediaDrm APIs provide the ability to securely deliver session keys from * an operator's session key server to a client device, based on the factory-installed * root of trust, and then perform encrypt, decrypt, sign and verify operations * with the session key on arbitrary user data. *

* The CryptoSession class implements generic encrypt/decrypt/sign/verify methods * based on the established session keys. These keys are exchanged using the * getKeyRequest/provideKeyResponse methods. *

* Applications of this capability could include securing various types of * purchased or private content, such as applications, books and other media, * photos or media delivery protocols. *

* Operators can create session key servers that are functionally similar to a * license key server, except that instead of receiving license key requests and * providing encrypted content keys which are used specifically to decrypt A/V media * content, the session key server receives session key requests and provides * encrypted session keys which can be used for general purpose crypto operations. *

* A CryptoSession is obtained using {@link #getCryptoSession} */ public final class CryptoSession { private byte[] mSessionId; CryptoSession(@NonNull byte[] sessionId, @NonNull String cipherAlgorithm, @NonNull String macAlgorithm) { mSessionId = sessionId; setCipherAlgorithmNative(MediaDrm.this, sessionId, cipherAlgorithm); setMacAlgorithmNative(MediaDrm.this, sessionId, macAlgorithm); } /** * Encrypt data using the CryptoSession's cipher algorithm * * @param keyid specifies which key to use * @param input the data to encrypt * @param iv the initialization vector to use for the cipher */ @NonNull public byte[] encrypt( @NonNull byte[] keyid, @NonNull byte[] input, @NonNull byte[] iv) { return encryptNative(MediaDrm.this, mSessionId, keyid, input, iv); } /** * Decrypt data using the CryptoSessions's cipher algorithm * * @param keyid specifies which key to use * @param input the data to encrypt * @param iv the initialization vector to use for the cipher */ @NonNull public byte[] decrypt( @NonNull byte[] keyid, @NonNull byte[] input, @NonNull byte[] iv) { return decryptNative(MediaDrm.this, mSessionId, keyid, input, iv); } /** * Sign data using the CryptoSessions's mac algorithm. * * @param keyid specifies which key to use * @param message the data for which a signature is to be computed */ @NonNull public byte[] sign(@NonNull byte[] keyid, @NonNull byte[] message) { return signNative(MediaDrm.this, mSessionId, keyid, message); } /** * Verify a signature using the CryptoSessions's mac algorithm. Return true * if the signatures match, false if they do no. * * @param keyid specifies which key to use * @param message the data to verify * @param signature the reference signature which will be compared with the * computed signature */ public boolean verify( @NonNull byte[] keyid, @NonNull byte[] message, @NonNull byte[] signature) { return verifyNative(MediaDrm.this, mSessionId, keyid, message, signature); } }; /** * Obtain a CryptoSession object which can be used to encrypt, decrypt, * sign and verify messages or data using the session keys established * for the session using methods {@link #getKeyRequest} and * {@link #provideKeyResponse} using a session key server. * * @param sessionId the session ID for the session containing keys * to be used for encrypt, decrypt, sign and/or verify * @param cipherAlgorithm the algorithm to use for encryption and * decryption ciphers. The algorithm string conforms to JCA Standard * Names for Cipher Transforms and is case insensitive. For example * "AES/CBC/NoPadding". * @param macAlgorithm the algorithm to use for sign and verify * The algorithm string conforms to JCA Standard Names for Mac * Algorithms and is case insensitive. For example "HmacSHA256". *

* The list of supported algorithms for a DRM engine plugin can be obtained * using the method {@link #getPropertyString} with the property name * "algorithms". */ public CryptoSession getCryptoSession( @NonNull byte[] sessionId, @NonNull String cipherAlgorithm, @NonNull String macAlgorithm) { return new CryptoSession(sessionId, cipherAlgorithm, macAlgorithm); } /** * Contains the opaque data an app uses to request a certificate from a provisioning * server * * @hide - not part of the public API at this time */ public static final class CertificateRequest { private byte[] mData; private String mDefaultUrl; CertificateRequest(@NonNull byte[] data, @NonNull String defaultUrl) { mData = data; mDefaultUrl = defaultUrl; } /** * Get the opaque message data */ @NonNull public byte[] getData() { return mData; } /** * Get the default URL to use when sending the certificate request * message to a server, if known. The app may prefer to use a different * certificate server URL obtained from other sources. */ @NonNull public String getDefaultUrl() { return mDefaultUrl; } } /** * Generate a certificate request, specifying the certificate type * and authority. The response received should be passed to * provideCertificateResponse. * * @param certType Specifies the certificate type. * * @param certAuthority is passed to the certificate server to specify * the chain of authority. * * @hide - not part of the public API at this time */ @NonNull public CertificateRequest getCertificateRequest( @CertificateType int certType, @NonNull String certAuthority) { ProvisionRequest provisionRequest = getProvisionRequestNative(certType, certAuthority); return new CertificateRequest(provisionRequest.getData(), provisionRequest.getDefaultUrl()); } /** * Contains the wrapped private key and public certificate data associated * with a certificate. * * @hide - not part of the public API at this time */ public static final class Certificate { Certificate() {} /** * Get the wrapped private key data */ @NonNull public byte[] getWrappedPrivateKey() { if (mWrappedKey == null) { // this should never happen as mWrappedKey is initialized in // JNI after construction of the KeyRequest object. The check // is needed here to guarantee @NonNull annotation. throw new RuntimeException("Cerfificate is not initialized"); } return mWrappedKey; } /** * Get the PEM-encoded certificate chain */ @NonNull public byte[] getContent() { if (mCertificateData == null) { // this should never happen as mCertificateData is initialized in // JNI after construction of the KeyRequest object. The check // is needed here to guarantee @NonNull annotation. throw new RuntimeException("Cerfificate is not initialized"); } return mCertificateData; } private byte[] mWrappedKey; private byte[] mCertificateData; } /** * Process a response from the certificate server. The response * is obtained from an HTTP Post to the url provided by getCertificateRequest. *

* The public X509 certificate chain and wrapped private key are returned * in the returned Certificate objec. The certificate chain is in PEM format. * The wrapped private key should be stored in application private * storage, and used when invoking the signRSA method. * * @param response the opaque certificate response byte array to provide to the * DRM engine plugin. * * @throws DeniedByServerException if the response indicates that the * server rejected the request * * @hide - not part of the public API at this time */ @NonNull public Certificate provideCertificateResponse(@NonNull byte[] response) throws DeniedByServerException { return provideProvisionResponseNative(response); } @NonNull private static final native byte[] signRSANative( @NonNull MediaDrm drm, @NonNull byte[] sessionId, @NonNull String algorithm, @NonNull byte[] wrappedKey, @NonNull byte[] message); /** * Sign data using an RSA key * * @param sessionId a sessionId obtained from openSession on the MediaDrm object * @param algorithm the signing algorithm to use, e.g. "PKCS1-BlockType1" * @param wrappedKey - the wrapped (encrypted) RSA private key obtained * from provideCertificateResponse * @param message the data for which a signature is to be computed * * @hide - not part of the public API at this time */ @NonNull public byte[] signRSA( @NonNull byte[] sessionId, @NonNull String algorithm, @NonNull byte[] wrappedKey, @NonNull byte[] message) { return signRSANative(this, sessionId, algorithm, wrappedKey, message); } @Override protected void finalize() { native_finalize(); } public native final void release(); private static native final void native_init(); private native final void native_setup(Object mediadrm_this, byte[] uuid, String appPackageName); private native final void native_finalize(); static { System.loadLibrary("media_jni"); native_init(); } }