Skip to content

Commit

Permalink
[EME for Android] Persist key type
Browse files Browse the repository at this point in the history
Record whether the license is released or not in persistent storage. Add
a new field "key_type" to persistent storage for each session. If the
license is installed to the device successfully, we update the field
with KEY_TYPE_OFFLINE. If the license is released, we should update the
key type to KEY_TYPE_RELEASE to reflect this change. In case the JS
doesn't provide any response from server and calls "load" again, we can
check the key type to avoid restore released keys, which may lead to an
exception.

Bug: 883895
Test: link in the bug
Change-Id: I4672bd0d640b2fd9faa8c6feae45894bd1aee062
Reviewed-on: https://chromium-review.googlesource.com/c/1327171
Commit-Queue: Yuchen Liu <yucliu@chromium.org>
Reviewed-by: Xiaohan Wang <xhwang@chromium.org>
Reviewed-by: Will Harris <wfh@chromium.org>
Cr-Commit-Position: refs/heads/master@{#610176}
  • Loading branch information
Yuchen Liu authored and Commit Bot committed Nov 21, 2018
1 parent 2f81a12 commit b37b0ee
Show file tree
Hide file tree
Showing 16 changed files with 228 additions and 82 deletions.
53 changes: 47 additions & 6 deletions components/cdm/browser/media_drm_storage_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/browser/navigation_handle.h"
#include "media/base/android/media_drm_key_type.h"

// The storage will be managed by PrefService. All data will be stored in a
// dictionary under the key "media.media_drm_storage". The dictionary is
Expand All @@ -25,7 +26,8 @@
// $session_id: {
// "key_set_id": $key_set_id,
// "mime_type": $mime_type,
// "creation_time": $creation_time
// "creation_time": $creation_time,
// "key_type": $key_type (enum of MediaDrmKeyType)
// },
// # more session_id map...
// }
Expand All @@ -48,8 +50,30 @@ const char kCreationTime[] = "creation_time";
const char kSessions[] = "sessions";
const char kKeySetId[] = "key_set_id";
const char kMimeType[] = "mime_type";
const char kKeyType[] = "key_type";
const char kOriginId[] = "origin_id";

bool GetMediaDrmKeyTypeFromDict(const base::Value& dict,
media::MediaDrmKeyType* value_out) {
DCHECK(dict.is_dict());
DCHECK(value_out);

const base::Value* value =
dict.FindKeyOfType(kKeyType, base::Value::Type::INTEGER);
if (!value)
return false;

int key_type = value->GetInt();
if (key_type < static_cast<int>(media::MediaDrmKeyType::UNKNOWN) ||
key_type > static_cast<int>(media::MediaDrmKeyType::MAX)) {
DVLOG(1) << "Corrupted key type.";
return false;
}

*value_out = static_cast<media::MediaDrmKeyType>(key_type);
return true;
}

bool GetStringFromDict(const base::Value& dict,
const std::string& key,
std::string* value_out) {
Expand Down Expand Up @@ -139,8 +163,9 @@ class OriginData {
class SessionData {
public:
SessionData(const std::vector<uint8_t>& key_set_id,
const std::string& mime_type)
: SessionData(key_set_id, mime_type, base::Time::Now()) {}
const std::string& mime_type,
media::MediaDrmKeyType key_type)
: SessionData(key_set_id, mime_type, key_type, base::Time::Now()) {}

base::Time creation_time() const { return creation_time_; }

Expand All @@ -152,13 +177,14 @@ class SessionData {
reinterpret_cast<const char*>(key_set_id_.data()),
key_set_id_.size())));
dict.SetKey(kMimeType, base::Value(mime_type_));
dict.SetKey(kKeyType, base::Value(static_cast<int>(key_type_)));
dict.SetKey(kCreationTime, base::Value(creation_time_.ToDoubleT()));

return dict;
}

media::mojom::SessionDataPtr ToMojo() const {
return media::mojom::SessionData::New(key_set_id_, mime_type_);
return media::mojom::SessionData::New(key_set_id_, mime_type_, key_type_);
}

// Convert |session_dict| to SessionData. |session_dict| contains information
Expand All @@ -180,22 +206,31 @@ class SessionData {
if (!GetCreationTimeFromDict(session_dict, &time))
return nullptr;

media::MediaDrmKeyType key_type;
if (!GetMediaDrmKeyTypeFromDict(session_dict, &key_type)) {
DVLOG(1) << "Missing key type.";
key_type = media::MediaDrmKeyType::UNKNOWN;
}

return base::WrapUnique(
new SessionData(std::vector<uint8_t>(key_set_id_string.begin(),
key_set_id_string.end()),
std::move(mime_type), time));
std::move(mime_type), key_type, time));
}

private:
SessionData(std::vector<uint8_t> key_set_id,
std::string mime_type,
media::MediaDrmKeyType key_type,
base::Time time)
: key_set_id_(std::move(key_set_id)),
mime_type_(std::move(mime_type)),
key_type_(key_type),
creation_time_(time) {}

std::vector<uint8_t> key_set_id_;
std::string mime_type_;
media::MediaDrmKeyType key_type_;
base::Time creation_time_;
};

Expand Down Expand Up @@ -504,8 +539,14 @@ void MediaDrmStorageImpl::SavePersistentSession(
DVLOG_IF(1, sessions_dict->FindKey(session_id))
<< __func__ << ": Session ID already exists and will be replaced.";

// The key type of the session should be valid. MeidaDrmKeyType::MIN/UNKNOWN
// is an invalid type and caller should never pass it here.
DCHECK_GT(session_data->key_type, media::MediaDrmKeyType::MIN);
DCHECK_LE(session_data->key_type, media::MediaDrmKeyType::MAX);

sessions_dict->SetKey(
session_id, SessionData(session_data->key_set_id, session_data->mime_type)
session_id, SessionData(session_data->key_set_id, session_data->mime_type,
session_data->key_type)
.ToDictValue());

std::move(callback).Run(true);
Expand Down
7 changes: 5 additions & 2 deletions components/cdm/browser/media_drm_storage_impl_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,18 @@ class MediaDrmStorageImplTest : public content::RenderViewHostTestHarness {
const std::string& mime_type,
bool success = true) {
media_drm_storage_->SavePersistentSession(
session_id, SessionData(key_set_id, mime_type), ExpectResult(success));
session_id,
SessionData(key_set_id, mime_type, media::MediaDrmKeyType::OFFLINE),
ExpectResult(success));
}

void LoadPersistentSession(const std::string& session_id,
const std::vector<uint8_t>& expected_key_set_id,
const std::string& expected_mime_type) {
media_drm_storage_->LoadPersistentSession(
session_id, ExpectResult(std::make_unique<SessionData>(
expected_key_set_id, expected_mime_type)));
expected_key_set_id, expected_mime_type,
media::MediaDrmKeyType::OFFLINE)));
}

void LoadPersistentSessionAndExpectFailure(const std::string& session_id) {
Expand Down
1 change: 1 addition & 0 deletions media/base/android/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ if (is_android) {
"media_drm_bridge_delegate.h",
"media_drm_bridge_factory.cc",
"media_drm_bridge_factory.h",
"media_drm_key_type.h",
"media_drm_storage.cc",
"media_drm_storage.h",
"media_drm_storage_bridge.cc",
Expand Down
89 changes: 48 additions & 41 deletions media/base/android/java/src/org/chromium/media/MediaDrmBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,6 @@ public class MediaDrmBridge {
private static final String ENABLE = "enable";
private static final long INVALID_NATIVE_MEDIA_DRM_BRIDGE = 0;

// Error message returned by MediaDrm functions.
private static final String MEDIA_DRM_ERROR_LICENSE_RELEASED =
"android.media.MediaDrm.error_neg_2948";

// Scheme UUID for Widevine. See http://dashif.org/identifiers/protection/
private static final UUID WIDEVINE_UUID =
UUID.fromString("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed");
Expand Down Expand Up @@ -927,7 +923,26 @@ private void loadSessionWithLoadedStorage(SessionId sessionId, final long promis

mSessionManager.setDrmId(sessionId, drmId);
assert Arrays.equals(sessionId.drmId(), drmId);
assert mSessionManager.get(sessionId).keyType() == MediaDrm.KEY_TYPE_OFFLINE;

SessionInfo sessionInfo = mSessionManager.get(sessionId);

// If persistent license (KEY_TYPE_OFFLINE) is released but we don't receive the ack
// from the server, we should avoid restoring the keys. Report success to JS so that
// they can release it again.
if (sessionInfo.keyType() == MediaDrm.KEY_TYPE_RELEASE) {
Log.w(TAG, "Persistent license is waiting for release ack.");
onPromiseResolvedWithSession(promiseId, sessionId);

// Report keystatuseschange event to JS. Ideally we should report the event with
// list of known key IDs. However we can't get the key IDs from MediaDrm. Just
// report with dummy key IDs.
onSessionKeysChange(sessionId,
getDummyKeysInfo(MediaDrm.KeyStatus.STATUS_EXPIRED).toArray(),
false /* hasAdditionalUsableKey */, true /* isKeyRelease */);
return;
}

assert sessionInfo.keyType() == MediaDrm.KEY_TYPE_OFFLINE;

// Defer event handlers until license is loaded.
assert mSessionEventDeferrer == null;
Expand All @@ -936,46 +951,20 @@ private void loadSessionWithLoadedStorage(SessionId sessionId, final long promis
assert sessionId.keySetId() != null;
mMediaDrm.restoreKeys(sessionId.drmId(), sessionId.keySetId());

onPersistentLicenseLoaded(sessionId, promiseId);
onPromiseResolvedWithSession(promiseId, sessionId);

mSessionEventDeferrer.fire();
mSessionEventDeferrer = null;
} catch (android.media.NotProvisionedException e) {
// If device isn't provisioned, storage loading should fail.
Log.w(TAG, "Persistent license load fail because origin isn't provisioned.");
onPersistentLicenseLoadFail(sessionId, promiseId);
} catch (java.lang.IllegalStateException e) {
assert sessionId.drmId() != null;

// If persistent license (KEY_TYPE_OFFLINE) is released but we don't receive the ack
// from the server, loading the key again will fail. Report success to JS so that
// they can release it again.
if (e instanceof MediaDrm.MediaDrmStateException) {
MediaDrm.MediaDrmStateException stateException =
(MediaDrm.MediaDrmStateException) e;
if (stateException.getDiagnosticInfo().equals(MEDIA_DRM_ERROR_LICENSE_RELEASED)) {
Log.w(TAG, "Persistent license is waiting for release ack.");
onPersistentLicenseLoaded(sessionId, promiseId);

// Report keystatuseschange event to JS.
onSessionKeysChange(sessionId,
getDummyKeysInfo(MediaDrm.KeyStatus.STATUS_EXPIRED).toArray(),
false /* hasAdditionalUsableKey */, false /* isKeyRelease */);
return;
}
}

onPersistentLicenseLoadFail(sessionId, promiseId);
}
}

private void onPersistentLicenseLoaded(SessionId sessionId, long promiseId) {
assert Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;

onPromiseResolvedWithSession(promiseId, sessionId);

assert mSessionEventDeferrer != null;
mSessionEventDeferrer.fire();
mSessionEventDeferrer = null;
}

private void onPersistentLicenseNoExist(long promiseId) {
// Chromium CDM API requires resolve the promise with empty session id for non-exist
// license. See media/base/content_decryption_module.h LoadSession for more details.
Expand Down Expand Up @@ -1004,7 +993,7 @@ public void onResult(Boolean success) {
* when the session is updated with a license release response.
*/
@CalledByNative
private void removeSession(byte[] emeId, long promiseId) {
private void removeSession(byte[] emeId, final long promiseId) {
Log.d(TAG, "removeSession()");
SessionId sessionId = getSessionIdByEmeId(emeId);

Expand All @@ -1013,21 +1002,39 @@ private void removeSession(byte[] emeId, long promiseId) {
return;
}

SessionInfo sessionInfo = mSessionManager.get(sessionId);
if (sessionInfo.keyType() != MediaDrm.KEY_TYPE_OFFLINE) {
final SessionInfo sessionInfo = mSessionManager.get(sessionId);
if (sessionInfo.keyType() == MediaDrm.KEY_TYPE_STREAMING) {
// TODO(yucliu): Support 'remove' of temporary session.
onPromiseRejected(promiseId, "Removing temporary session isn't implemented");
return;
}

assert sessionId.keySetId() != null;

mSessionManager.markKeyReleased(sessionId);
// Persist the key type before removing the keys completely.
// 1. If we fails to persist the key type, both the persistent storage and MediaDrm think
// the keys are alive. JS can just remove the session again.
// 2. If we are able to persist the key type but don't get the callback, persistent storage
// thinks keys are removed but MediaDrm thinks keys are alive. JS thinks keys are removed
// next time it loads the keys, which matches the expectation of this function.
mSessionManager.setKeyType(sessionId, MediaDrm.KEY_TYPE_RELEASE, new Callback<Boolean>() {
@Override
public void onResult(Boolean success) {
if (!success) {
onPromiseRejected(promiseId, "Fail to update persistent storage");
return;
}

doRemoveSession(sessionId, sessionInfo.mimeType(), promiseId);
}
});
}

private void doRemoveSession(SessionId sessionId, String mimeType, long promiseId) {
try {
// Get key release request.
MediaDrm.KeyRequest request = getKeyRequest(
sessionId, null, sessionInfo.mimeType(), MediaDrm.KEY_TYPE_RELEASE, null);
MediaDrm.KeyRequest request =
getKeyRequest(sessionId, null, mimeType, MediaDrm.KEY_TYPE_RELEASE, null);

if (request == null) {
onPromiseRejected(promiseId, "Fail to generate key release request");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ private void setKeyType(int keyType) {
private PersistentInfo toPersistentInfo() {
assert mSessionId.keySetId() != null;

return new PersistentInfo(mSessionId.emeId(), mSessionId.keySetId(), mMimeType);
return new PersistentInfo(
mSessionId.emeId(), mSessionId.keySetId(), mMimeType, mKeyType);
}

private static SessionInfo fromPersistentInfo(PersistentInfo persistentInfo) {
Expand All @@ -182,7 +183,18 @@ private static SessionInfo fromPersistentInfo(PersistentInfo persistentInfo) {

SessionId sessionId = new SessionId(
persistentInfo.emeId(), null /* drmId */, persistentInfo.keySetId());
return new SessionInfo(sessionId, persistentInfo.mimeType(), MediaDrm.KEY_TYPE_OFFLINE);
return new SessionInfo(sessionId, persistentInfo.mimeType(),
getKeyTypeFromPersistentInfo(persistentInfo));
}

private static int getKeyTypeFromPersistentInfo(PersistentInfo persistentInfo) {
int keyType = persistentInfo.keyType();
if (keyType == MediaDrm.KEY_TYPE_OFFLINE || keyType == MediaDrm.KEY_TYPE_RELEASE) {
return keyType;
}

// Key type is missing. Use OFFLINE by default.
return MediaDrm.KEY_TYPE_OFFLINE;
}
}

Expand Down Expand Up @@ -238,13 +250,13 @@ void setKeySetId(SessionId sessionId, byte[] keySetId, Callback<Boolean> callbac
* Mark key as released. It should only be called for persistent license
* session.
*/
void markKeyReleased(SessionId sessionId) {
void setKeyType(SessionId sessionId, int keyType, Callback<Boolean> callback) {
SessionInfo info = get(sessionId);

assert info != null;
assert info.keyType() == MediaDrm.KEY_TYPE_OFFLINE;

info.setKeyType(MediaDrm.KEY_TYPE_RELEASE);
info.setKeyType(keyType);
mStorage.saveInfo(info.toPersistentInfo(), callback);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,20 @@ static class PersistentInfo {
// Mime type for the license.
private final String mMimeType;

// Key type of session. It can be any value. Caller should check it before actual using it.
private final int mKeyType;

@CalledByNative("PersistentInfo")
private static PersistentInfo create(byte[] emeId, byte[] keySetId, String mime) {
return new PersistentInfo(emeId, keySetId, mime);
private static PersistentInfo create(
byte[] emeId, byte[] keySetId, String mime, int keyType) {
return new PersistentInfo(emeId, keySetId, mime, keyType);
}

PersistentInfo(byte[] emeId, byte[] keySetId, String mime) {
PersistentInfo(byte[] emeId, byte[] keySetId, String mime, int keyType) {
mEmeId = emeId;
mKeySetId = keySetId;
mMimeType = mime;
mKeyType = keyType;
}

@CalledByNative("PersistentInfo")
Expand All @@ -63,6 +68,11 @@ byte[] keySetId() {
String mimeType() {
return mMimeType;
}

@CalledByNative("PersistentInfo")
int keyType() {
return mKeyType;
}
}

MediaDrmStorageBridge(long nativeMediaDrmStorageBridge) {
Expand Down
Loading

0 comments on commit b37b0ee

Please sign in to comment.