Files
android/libwvdrmengine/cdm/core/src/policy_engine.cpp
John W. Bruce b8e31487a4 Unified State-Changing API for LicenseKeyStatus
(This is a merge of http://go/wvgerrit/31040)

Because the Policy Engine was only consulting the result of the Max-Res
Decode check when it was in kLicenseStateCanPlay and not in other states
that imply kKeyStatusUsable, like kLicenseStateWaitingLicenseUpdate, the
Max-Res Decode results would not be honored during the interval between
requesting a renewal and receiving the result. (Or until the key
expired.) This was particularly problematic for keys with renewal delays
less than ten seconds long, which would freeze the Max-Res state before
it had a chance to update for the first time, effectively disabling
Max-Res Decode until renewal was received.

Fixing this required changing how the Policy Engine and the
LicenseKeyStatus objects communicate about the changing usability state
of the LicenseKeyStatus objects. Before, a call to ApplyConstraints()
might calculate a Max-Res failure, but this failure would be pending
until the Policy Engine deigned to call ApplyStatusChange() again.
Without a call to ApplyStatusChange(), it could pend forever. This put a
burden on the PolicyEngine to poll the LicenseKeys with redundant
ApplyStatusChange() calls using the same CdmKeyStatus that the keys were
already in, just in case Max-Res had changed since the last necessary
call to ApplyStatusChange().

If the Policy Engine got the timing of these calls wrong, it would
result in Max-Res results being ignored. (as in the linked bug) If it
ever polled with the wrong CdmKeyStatus, it would update the
LicenseKeys' status when it did not mean to. It would be preferable if
this polling were not needed, so that the Policy Engine couldn't get it
wrong.

This patch changes the API between these classes so that when Max-Res
fails, the state change can be reported immediately instead of pending
until ApplyStatusChange() is called, eliminating the need for polling.
All state changes to the LicenseKeyStatus objects go through a unified
ApplyStatusChange() method that can update the CdmKeyStatus, resolution,
and/or HDCP level and report any resulting usability changes
immediately. This patch updates the unit tests to exercise this new API
instead of the old API.

Previously, the linked bug slipped past our unit tests because we only
test unrenewable, streaming licenses against Max-Res. This patch adds
several more variants to
policy_engine_constraints_unittest so that it tests six kinds of
license to provide better coverage.

Bug: 62393949
Test: build_and_run_all_unit_tests
Change-Id: I0dfdbf6b8ea39abb446089aef5f6ea0502e9b4c6
2017-08-29 16:37:49 -07:00

472 lines
16 KiB
C++

// Copyright 2013 Google Inc. All Rights Reserved.
#include "policy_engine.h"
#include <limits.h>
#include <sstream>
#include "clock.h"
#include "log.h"
#include "properties.h"
#include "string_conversions.h"
#include "wv_cdm_constants.h"
#include "wv_cdm_event_listener.h"
using video_widevine::License;
namespace {
const int64_t kHdcpCheckInterval = 10;
} // namespace
namespace wvcdm {
PolicyEngine::PolicyEngine(CdmSessionId session_id,
WvCdmEventListener* event_listener,
CryptoSession* crypto_session)
: license_state_(kLicenseStateInitial),
license_start_time_(0),
playback_start_time_(0),
last_playback_time_(0),
last_expiry_time_(0),
grace_period_end_time_(0),
last_expiry_time_set_(false),
was_expired_on_load_(false),
next_renewal_time_(0),
session_id_(session_id),
event_listener_(event_listener),
license_keys_(new LicenseKeys),
clock_(new Clock) {
InitDevice(crypto_session);
}
PolicyEngine::~PolicyEngine() {}
bool PolicyEngine::CanDecryptContent(const KeyId& key_id) {
if (license_keys_->IsContentKey(key_id)) {
return license_keys_->CanDecryptContent(key_id);
} else {
LOGE("PolicyEngine::CanDecryptContent Key '%s' not in license.",
b2a_hex(key_id).c_str());
return false;
}
}
void PolicyEngine::InitDevice(CryptoSession* crypto_session) {
current_resolution_ = kNoResolution;
next_device_check_ = 0;
crypto_session_ = crypto_session;
}
void PolicyEngine::CheckDevice(int64_t current_time) {
if (current_time < next_device_check_) {
return;
}
if (!license_keys_->Empty() && current_resolution_ != kNoResolution) {
CryptoSession::HdcpCapability current_hdcp_level;
CryptoSession::HdcpCapability ignored;
if (!crypto_session_->GetHdcpCapabilities(&current_hdcp_level, &ignored)) {
current_hdcp_level = HDCP_NONE;
}
bool new_usable_keys = false;
bool keys_changed =
license_keys_->ApplyStatusChange(NULL, // new_status
&current_resolution_,
&current_hdcp_level,
&new_usable_keys);
NotifyIfKeysChanged(keys_changed, new_usable_keys);
next_device_check_ = current_time + kHdcpCheckInterval;
}
}
void PolicyEngine::OnTimerEvent() {
int64_t current_time = clock_->GetCurrentTime();
// If we have passed the grace period, the expiration will update.
if (grace_period_end_time_ == 0 && HasPlaybackStarted(current_time)) {
grace_period_end_time_ = playback_start_time_;
NotifyExpirationUpdate(current_time);
}
// License expiration trumps all.
if (HasLicenseOrPlaybackDurationExpired(current_time) &&
license_state_ != kLicenseStateExpired) {
license_state_ = kLicenseStateExpired;
UpdateKeyStatus(kKeyStatusExpired);
return;
}
// Check device conditions that affect playability (HDCP, resolution)
CheckDevice(current_time);
// Test to determine if renewal should be attempted.
bool renewal_needed = false;
switch (license_state_) {
case kLicenseStateCanPlay: {
if (HasRenewalDelayExpired(current_time)) renewal_needed = true;
break;
}
case kLicenseStateNeedRenewal: {
renewal_needed = true;
break;
}
case kLicenseStateWaitingLicenseUpdate: {
if (HasRenewalRetryIntervalExpired(current_time)) renewal_needed = true;
break;
}
case kLicenseStatePending: {
if (current_time >= license_start_time_) {
license_state_ = kLicenseStateCanPlay;
UpdateKeyStatus(kKeyStatusUsable);
}
break;
}
case kLicenseStateInitial:
case kLicenseStateExpired: {
break;
}
default: {
license_state_ = kLicenseStateExpired;
UpdateKeyStatus(kKeyStatusInternalError);
break;
}
}
if (renewal_needed) {
UpdateRenewalRequest(current_time);
if (event_listener_) event_listener_->OnSessionRenewalNeeded(session_id_);
}
}
void PolicyEngine::SetLicense(const License& license) {
license_id_.Clear();
license_id_.CopyFrom(license.id());
policy_.Clear();
license_keys_->SetFromLicense(license);
UpdateLicense(license);
}
void PolicyEngine::SetLicenseForRelease(const License& license) {
license_id_.Clear();
license_id_.CopyFrom(license.id());
policy_.Clear();
// Expire any old keys.
UpdateKeyStatus(kKeyStatusExpired);
UpdateLicense(license);
}
void PolicyEngine::UpdateLicense(const License& license) {
if (!license.has_policy()) return;
if (kLicenseStateExpired == license_state_) {
LOGD("PolicyEngine::UpdateLicense: updating an expired license");
}
policy_.MergeFrom(license.policy());
// some basic license validation
// license start time needs to be specified in the initial response
if (!license.has_license_start_time()) return;
// if renewal, discard license if version has not been updated
if (license_state_ != kLicenseStateInitial) {
if (license.id().version() > license_id_.version())
license_id_.CopyFrom(license.id());
else
return;
}
// Update time information
license_start_time_ = license.license_start_time();
next_renewal_time_ = license_start_time_ + policy_.renewal_delay_seconds();
int64_t current_time = clock_->GetCurrentTime();
if (!policy_.can_play() ||
HasLicenseOrPlaybackDurationExpired(current_time)) {
license_state_ = kLicenseStateExpired;
UpdateKeyStatus(kKeyStatusExpired);
return;
}
// Update state
if (current_time >= license_start_time_) {
license_state_ = kLicenseStateCanPlay;
UpdateKeyStatus(kKeyStatusUsable);
} else {
license_state_ = kLicenseStatePending;
UpdateKeyStatus(kKeyStatusPending);
}
NotifyExpirationUpdate(current_time);
}
void PolicyEngine::BeginDecryption() {
if (playback_start_time_ == 0) {
switch (license_state_) {
case kLicenseStateCanPlay:
case kLicenseStateNeedRenewal:
case kLicenseStateWaitingLicenseUpdate:
playback_start_time_ = clock_->GetCurrentTime();
last_playback_time_ = playback_start_time_;
if (policy_.play_start_grace_period_seconds() == 0)
grace_period_end_time_ = playback_start_time_;
if (policy_.renew_with_usage()) {
license_state_ = kLicenseStateNeedRenewal;
}
NotifyExpirationUpdate(playback_start_time_);
break;
case kLicenseStateInitial:
case kLicenseStatePending:
case kLicenseStateExpired:
default:
break;
}
}
}
void PolicyEngine::DecryptionEvent() {
last_playback_time_ = clock_->GetCurrentTime();
}
void PolicyEngine::NotifyResolution(uint32_t width, uint32_t height) {
SetDeviceResolution(width, height);
}
void PolicyEngine::NotifySessionExpiration() {
license_state_ = kLicenseStateExpired;
UpdateKeyStatus(kKeyStatusExpired);
}
CdmResponseType PolicyEngine::Query(CdmQueryMap* query_response) {
std::stringstream ss;
int64_t current_time = clock_->GetCurrentTime();
if (license_state_ == kLicenseStateInitial) {
query_response->clear();
return NO_ERROR;
}
(*query_response)[QUERY_KEY_LICENSE_TYPE] =
license_id_.type() == video_widevine::STREAMING
? QUERY_VALUE_STREAMING
: QUERY_VALUE_OFFLINE;
(*query_response)[QUERY_KEY_PLAY_ALLOWED] =
policy_.can_play() ? QUERY_VALUE_TRUE : QUERY_VALUE_FALSE;
(*query_response)[QUERY_KEY_PERSIST_ALLOWED] =
policy_.can_persist() ? QUERY_VALUE_TRUE : QUERY_VALUE_FALSE;
(*query_response)[QUERY_KEY_RENEW_ALLOWED] =
policy_.can_renew() ? QUERY_VALUE_TRUE : QUERY_VALUE_FALSE;
ss << GetLicenseOrRentalDurationRemaining(current_time);
(*query_response)[QUERY_KEY_LICENSE_DURATION_REMAINING] = ss.str();
ss.str("");
ss << GetPlaybackDurationRemaining(current_time);
(*query_response)[QUERY_KEY_PLAYBACK_DURATION_REMAINING] = ss.str();
(*query_response)[QUERY_KEY_RENEWAL_SERVER_URL] = policy_.renewal_server_url();
return NO_ERROR;
}
CdmResponseType PolicyEngine::QueryKeyAllowedUsage(
const KeyId& key_id, CdmKeyAllowedUsage* key_usage) {
if (NULL == key_usage) {
return INVALID_PARAMETERS_ENG_12;
}
if (license_keys_->GetAllowedUsage(key_id, key_usage)) {
return NO_ERROR;
}
return KEY_NOT_FOUND_1;
}
bool PolicyEngine::GetSecondsSinceStarted(int64_t* seconds_since_started) {
if (playback_start_time_ == 0) return false;
*seconds_since_started = clock_->GetCurrentTime() - playback_start_time_;
return (*seconds_since_started >= 0) ? true : false;
}
bool PolicyEngine::GetSecondsSinceLastPlayed(
int64_t* seconds_since_last_played) {
if (last_playback_time_ == 0) return false;
*seconds_since_last_played = clock_->GetCurrentTime() - last_playback_time_;
return (*seconds_since_last_played >= 0) ? true : false;
}
int64_t PolicyEngine::GetLicenseOrPlaybackDurationRemaining() {
const int64_t current_time = clock_->GetCurrentTime();
const int64_t expiry_time =
GetExpiryTime(current_time,
/* ignore_soft_enforce_playback_duration */ false);
if (expiry_time == NEVER_EXPIRES) return LLONG_MAX;
if (expiry_time < current_time) return 0;
return expiry_time - current_time;
}
void PolicyEngine::RestorePlaybackTimes(int64_t playback_start_time,
int64_t last_playback_time,
int64_t grace_period_end_time) {
playback_start_time_ = (playback_start_time > 0) ? playback_start_time : 0;
last_playback_time_ = (last_playback_time > 0) ? last_playback_time : 0;
grace_period_end_time_ = grace_period_end_time;
if (policy_.play_start_grace_period_seconds() != 0) {
// If we are using grace period, we may need to override some of the values
// given to us by OEMCrypto. |grace_period_end_time| will be 0 if the grace
// period has not expired (effectively playback has not begun). Otherwise,
// |grace_period_end_time| contains the playback start time we should use.
playback_start_time_ = grace_period_end_time;
}
const int64_t current_time = clock_->GetCurrentTime();
const int64_t expiry_time =
GetExpiryTime(current_time,
/* ignore_soft_enforce_playback_duration */ true);
was_expired_on_load_ =
expiry_time != NEVER_EXPIRES && expiry_time < current_time;
NotifyExpirationUpdate(current_time);
}
void PolicyEngine::UpdateRenewalRequest(int64_t current_time) {
license_state_ = kLicenseStateWaitingLicenseUpdate;
next_renewal_time_ = current_time + policy_.renewal_retry_interval_seconds();
}
bool PolicyEngine::HasLicenseOrPlaybackDurationExpired(int64_t current_time) {
const int64_t expiry_time =
GetExpiryTime(current_time,
/* ignore_soft_enforce_playback_duration */ false);
return expiry_time != NEVER_EXPIRES && expiry_time <= current_time;
}
// For the policy time fields checked in the following methods, a value of 0
// indicates that there is no limit to the duration. If the fields are zero
// (including the hard limit) then these methods will return NEVER_EXPIRES.
int64_t PolicyEngine::GetHardLicenseExpiryTime() {
return policy_.license_duration_seconds() > 0
? license_start_time_ + policy_.license_duration_seconds()
: NEVER_EXPIRES;
}
int64_t PolicyEngine::GetRentalExpiryTime() {
const int64_t hard_limit = GetHardLicenseExpiryTime();
if (policy_.rental_duration_seconds() == 0) return hard_limit;
const int64_t expiry_time =
license_start_time_ + policy_.rental_duration_seconds();
if (hard_limit == NEVER_EXPIRES) return expiry_time;
return std::min(hard_limit, expiry_time);
}
int64_t PolicyEngine::GetExpiryTime(
int64_t current_time,
bool ignore_soft_enforce_playback_duration) {
if (!HasPlaybackStarted(current_time))
return GetRentalExpiryTime();
const int64_t hard_limit = GetHardLicenseExpiryTime();
if (policy_.playback_duration_seconds() == 0) return hard_limit;
if (!ignore_soft_enforce_playback_duration && !was_expired_on_load_ &&
policy_.soft_enforce_playback_duration()) {
return hard_limit;
}
const int64_t expiry_time =
playback_start_time_ + policy_.playback_duration_seconds();
if (hard_limit == NEVER_EXPIRES)
return expiry_time;
return std::min(hard_limit, expiry_time);
}
int64_t PolicyEngine::GetLicenseOrRentalDurationRemaining(
int64_t current_time) {
// This is only used in Query. This should return the time remaining on
// license_duration_seconds for streaming licenses and rental_duration_seconds
// for offline licenses.
if (HasLicenseOrPlaybackDurationExpired(current_time)) return 0;
const int64_t license_expiry_time = GetRentalExpiryTime();
if (license_expiry_time == NEVER_EXPIRES) return LLONG_MAX;
if (license_expiry_time < current_time) return 0;
const int64_t policy_license_duration = policy_.license_duration_seconds();
return std::min(license_expiry_time - current_time, policy_license_duration);
}
int64_t PolicyEngine::GetPlaybackDurationRemaining(int64_t current_time) {
// This is only used in Query. This should return playback_duration_seconds,
// or the time remaining on it if playing.
const int64_t playback_duration = policy_.playback_duration_seconds();
if (playback_duration == 0) return LLONG_MAX;
if (playback_start_time_ == 0) return playback_duration;
const int64_t playback_expiry_time = playback_duration + playback_start_time_;
if (playback_expiry_time < current_time) return 0;
const int64_t policy_playback_duration = policy_.playback_duration_seconds();
return std::min(playback_expiry_time - current_time,
policy_playback_duration);
}
bool PolicyEngine::HasRenewalDelayExpired(int64_t current_time) {
return policy_.can_renew() && (policy_.renewal_delay_seconds() > 0) &&
license_start_time_ + policy_.renewal_delay_seconds() <= current_time;
}
bool PolicyEngine::HasRenewalRecoveryDurationExpired(int64_t current_time) {
// NOTE: Renewal Recovery Duration is currently not used.
return (policy_.renewal_recovery_duration_seconds() > 0) &&
license_start_time_ + policy_.renewal_recovery_duration_seconds() <=
current_time;
}
bool PolicyEngine::HasRenewalRetryIntervalExpired(int64_t current_time) {
return policy_.can_renew() &&
(policy_.renewal_retry_interval_seconds() > 0) &&
next_renewal_time_ <= current_time;
}
void PolicyEngine::UpdateKeyStatus(CdmKeyStatus new_status) {
bool new_usable_keys = false;
bool keys_changed =
license_keys_->ApplyStatusChange(&new_status,
NULL, // new_resolution
NULL, // new_hdcp_level
&new_usable_keys);
NotifyIfKeysChanged(keys_changed, new_usable_keys);
}
void PolicyEngine::NotifyIfKeysChanged(bool keys_changed,
bool new_usable_keys) {
if (event_listener_ && keys_changed) {
CdmKeyStatusMap content_keys;
license_keys_->ExtractKeyStatuses(&content_keys);
event_listener_->OnSessionKeysChange(session_id_, content_keys,
new_usable_keys);
}
}
void PolicyEngine::NotifyExpirationUpdate(int64_t current_time) {
const int64_t expiry_time =
GetExpiryTime(current_time,
/* ignore_soft_enforce_playback_duration */ false);
if (!last_expiry_time_set_ || expiry_time != last_expiry_time_) {
last_expiry_time_ = expiry_time;
if (event_listener_)
event_listener_->OnExpirationUpdate(session_id_, expiry_time);
}
last_expiry_time_set_ = true;
}
void PolicyEngine::set_clock(Clock* clock) { clock_.reset(clock); }
} // namespace wvcdm