/*
* 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 android.media.MediaDrmException;
import java.lang.ref.WeakReference;
import java.util.UUID;
import java.util.HashMap;
import java.util.List;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Bundle;
import android.os.Parcel;
import android.util.Log;
/**
* MediaDrm can be used in conjunction with {@link android.media.MediaCrypto}
* to obtain keys for decrypting protected media data.
*
* 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.
*
*
*
Callbacks
* Applications may want to register for informational events in order
* to be informed of some internal state update during playback or streaming.
* Registration for these events is done via a call to
* {@link #setOnEventListener(OnInfoListener)}setOnInfoListener,
* 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).
*
* @hide -- don't expose yet
*/
public final class MediaDrm {
private final static String TAG = "MediaDrm";
private EventHandler mEventHandler;
private OnEventListener mOnEventListener;
private int mNativeContext;
/**
* 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(UUID uuid) {
return isCryptoSchemeSupportedNative(getByteArrayFromUUID(uuid));
}
private static final byte[] getByteArrayFromUUID(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(byte[] uuid);
/**
* Instantiate a MediaDrm object using opaque, crypto scheme specific
* data.
* @param uuid The UUID of the crypto scheme.
*/
public MediaDrm(UUID uuid) throws MediaDrmException {
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));
}
/**
* Register a callback to be invoked when an event occurs
*
* @param listener the callback that will be run
*/
public void setOnEventListener(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
* @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(MediaDrm md, byte[] sessionId, int event, int extra, byte[] data);
}
public static final int MEDIA_DRM_EVENT_PROVISION_REQUIRED = 1;
public static final int MEDIA_DRM_EVENT_KEY_REQUIRED = 2;
public static final int MEDIA_DRM_EVENT_KEY_EXPIRED = 3;
public static final int MEDIA_DRM_EVENT_VENDOR_DEFINED = 4;
private static final int DRM_EVENT = 200;
private class EventHandler extends Handler
{
private MediaDrm mMediaDrm;
public EventHandler(MediaDrm md, Looper looper) {
super(looper);
mMediaDrm = md;
}
@Override
public void handleMessage(Message msg) {
if (mMediaDrm.mNativeContext == 0) {
Log.w(TAG, "MediaDrm went away with unhandled events");
return;
}
switch(msg.what) {
case DRM_EVENT:
Log.i(TAG, "Drm event (" + msg.arg1 + "," + msg.arg2 + ")");
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;
}
mOnEventListener.onEvent(mMediaDrm, sessionId, msg.arg1, msg.arg2, data);
}
}
return;
default:
Log.e(TAG, "Unknown message type " + msg.what);
return;
}
}
}
/*
* Called from native code when an interesting event happens. 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(Object mediadrm_ref,
int eventType, int extra, Object obj)
{
MediaDrm md = (MediaDrm)((WeakReference)mediadrm_ref).get();
if (md == null) {
return;
}
if (md.mEventHandler != null) {
Message m = md.mEventHandler.obtainMessage(DRM_EVENT, eventType, extra, obj);
md.mEventHandler.sendMessage(m);
}
}
/**
* Open a new session with the MediaDrm object. A session ID is returned.
*/
public native byte[] openSession() throws MediaDrmException;
/**
* Close a session on the MediaDrm object that was previously opened
* with {@link #openSession}.
*/
public native void closeSession(byte[] sessionId) throws MediaDrmException;
public static final int MEDIA_DRM_KEY_TYPE_STREAMING = 1;
public static final int MEDIA_DRM_KEY_TYPE_OFFLINE = 2;
public static final int MEDIA_DRM_KEY_TYPE_RELEASE = 3;
public final class KeyRequest {
public KeyRequest() {}
public byte[] data;
public String defaultUrl;
};
/**
* 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 MEDIA_DRM_KEY_TYPE_STREAMING or MEDIA_DRM_KEY_TYPE_OFFLINE,
* scope should be set to the sessionId the keys will be provided to. When the keyType
* is MEDIA_DRM_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. init may be null when keyType is
* MEDIA_DRM_KEY_TYPE_RELEASE.
* @param mimeType identifies the mime type of the content
* @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.
*/
public native KeyRequest getKeyRequest(byte[] scope, byte[] init,
String mimeType, int keyType,
HashMap optionalParameters)
throws MediaDrmException;
/**
* A key response is received from the license server by the app, then it is
* provided to the DRM engine plugin using provideKeyResponse. The byte array
* returned is a keySetId that can be used to later restore the keys to a new
* session with the method {@link restoreKeys}, enabling offline key use.
*
* @param sessionId the session ID for the DRM session
* @param response the byte array response from the server
*/
public native byte[] provideKeyResponse(byte[] sessionId, byte[] response)
throws MediaDrmException;
/**
* 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(byte[] sessionId, byte[] keySetId)
throws MediaDrmException;
/**
* Remove the current keys from a session.
*
* @param sessionId the session ID for the DRM session
*/
public native void removeKeys(byte[] sessionId) throws MediaDrmException;
/**
* 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
*/
public native HashMap queryKeyStatus(byte[] sessionId)
throws MediaDrmException;
public final class ProvisionRequest {
public ProvisionRequest() {}
public byte[] data;
public String defaultUrl;
}
/**
* A provision request/response exchange occurs between the app and a provisioning
* server to retrieve a device certificate. If provisionining is required, the
* MEDIA_DRM_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.
*/
public native ProvisionRequest getProvisionRequest() throws MediaDrmException;
/**
* 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.
*/
public native void provideProvisionResponse(byte[] response)
throws MediaDrmException;
/**
* A means of enforcing the contractual requirement for a concurrent stream limit
* per subscriber across devices is provided via SecureStop. SecureStop is a means
* of securely monitoring the lifetime of sessions. Since playback on a device can
* be interrupted due to reboot, power failure, etc. a means of persisting the
* lifetime information on the device is needed.
*
* A signed version of the sessionID is written to persistent storage on the device
* when each MediaCrypto object is created. The sessionID is signed by the device
* private key to prevent tampering.
*
* 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().
*/
public native List getSecureStops() throws MediaDrmException;
/**
* Process the SecureStop server response message ssRelease. After authenticating
* the message, remove the SecureStops identiied in the response.
*
* @param ssRelease the server response indicating which secure stops to release
*/
public native void releaseSecureStops(byte[] ssRelease)
throws MediaDrmException;
/**
* Read a DRM engine plugin property value, given the property name string. There are
* several forms of property access functions, depending on the data type returned.
*
* Standard fields names are:
* vendor String - identifies the maker of the DRM engine plugin
* version String - identifies the version of the DRM engine plugin
* description String - describes the DRM engine plugin
* deviceUniqueId byte[] - The device unique identifier is established during device
* provisioning and provides a means of uniquely identifying
* each device
* algorithms String - a comma-separate 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 native String getPropertyString(String propertyName)
throws MediaDrmException;
public native byte[] getPropertyByteArray(String propertyName)
throws MediaDrmException;
/**
* Write a DRM engine plugin property value. There are several forms of
* property setting functions, depending on the data type being set.
*/
public native void setPropertyString(String propertyName, String value)
throws MediaDrmException;
public native void setPropertyByteArray(String propertyName, byte[] value)
throws MediaDrmException;
/**
* 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 provide the ability to do encrypt, decrypt, sign and verify
* 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.
*/
private static final native void setCipherAlgorithmNative(MediaDrm drm, byte[] sessionId,
String algorithm);
private static final native void setMacAlgorithmNative(MediaDrm drm, byte[] sessionId,
String algorithm);
private static final native byte[] encryptNative(MediaDrm drm, byte[] sessionId,
byte[] keyId, byte[] input, byte[] iv);
private static final native byte[] decryptNative(MediaDrm drm, byte[] sessionId,
byte[] keyId, byte[] input, byte[] iv);
private static final native byte[] signNative(MediaDrm drm, byte[] sessionId,
byte[] keyId, byte[] message);
private static final native boolean verifyNative(MediaDrm drm, byte[] sessionId,
byte[] keyId, byte[] message,
byte[] signature);
public final class CryptoSession {
private MediaDrm mDrm;
private byte[] mSessionId;
/**
* Construct a CryptoSession 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/PKCS5Padding".
*
* @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("algorithms")}
*/
public CryptoSession(MediaDrm drm, byte[] sessionId,
String cipherAlgorithm, String macAlgorithm)
throws MediaDrmException {
mSessionId = sessionId;
mDrm = drm;
setCipherAlgorithmNative(drm, sessionId, cipherAlgorithm);
setMacAlgorithmNative(drm, sessionId, macAlgorithm);
}
public byte[] encrypt(byte[] keyid, byte[] input, byte[] iv) {
return encryptNative(mDrm, mSessionId, keyid, input, iv);
}
public byte[] decrypt(byte[] keyid, byte[] input, byte[] iv) {
return decryptNative(mDrm, mSessionId, keyid, input, iv);
}
public byte[] sign(byte[] keyid, byte[] message) {
return signNative(mDrm, mSessionId, keyid, message);
}
public boolean verify(byte[] keyid, byte[] message, byte[] signature) {
return verifyNative(mDrm, mSessionId, keyid, message, signature);
}
};
public CryptoSession getCryptoSession(byte[] sessionId,
String cipherAlgorithm,
String macAlgorithm)
throws MediaDrmException {
return new CryptoSession(this, sessionId, cipherAlgorithm, macAlgorithm);
}
@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)
throws MediaDrmException;
private native final void native_finalize();
static {
System.loadLibrary("media_jni");
native_init();
}
}