(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
472 lines
16 KiB
C++
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(¤t_hdcp_level, &ignored)) {
|
|
current_hdcp_level = HDCP_NONE;
|
|
}
|
|
|
|
bool new_usable_keys = false;
|
|
bool keys_changed =
|
|
license_keys_->ApplyStatusChange(NULL, // new_status
|
|
¤t_resolution_,
|
|
¤t_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
|