Files
media_cas_client/plugin/src/widevine_cas_api.cpp
Lu Chen 66d8498d2c Regular update
Widevine CAS plugin updates include:
- Make Android session id little endian
- Rename ca_descriptor.proto to media_cas.proto
2020-11-30 12:45:07 -08:00

499 lines
17 KiB
C++

#include "widevine_cas_api.h"
#include <openssl/sha.h>
#include "cas_events.h"
#include "cas_util.h"
#include "license_protocol.pb.h"
#include "log.h"
#include "media_cas.pb.h"
#include "string_conversions.h"
#include "widevine_cas_session_map.h"
constexpr char kBasePathPrefix[] = "/data/vendor/mediacas/IDM/widevine/";
constexpr char kCertFileBase[] = "cert.bin";
constexpr char kLicenseFileNameSuffix[] = ".lic";
namespace {
bool ReadFileFromStorage(wvutil::FileSystem& file_system,
const std::string& filename, std::string* file_data) {
if (nullptr == file_data) {
return false;
}
if (!file_system.Exists(filename)) {
return false;
}
size_t filesize = file_system.FileSize(filename);
if (0 == filesize) {
return false;
}
file_data->resize(filesize);
std::unique_ptr<wvutil::File> file =
file_system.Open(filename, wvutil::FileSystem::kReadOnly);
if (nullptr == file) {
return false;
}
size_t bytes_read = file->Read(&(*file_data)[0], file_data->size());
if (bytes_read != filesize) {
return false;
}
return true;
}
bool RemoveFile(wvutil::FileSystem& file_system, const std::string& filename) {
if (!file_system.Exists(filename)) {
return false;
}
if (!file_system.Remove(filename)) {
return false;
}
return true;
}
bool StoreFile(wvutil::FileSystem& file_system, const std::string& filename,
const std::string& file_data) {
std::unique_ptr<wvutil::File> file(file_system.Open(
filename, wvutil::FileSystem::kTruncate | wvutil::FileSystem::kCreate));
if (nullptr == file) {
return false;
}
size_t bytes_written = file->Write(file_data.data(), file_data.size());
if (bytes_written != file_data.size()) {
return false;
}
return true;
}
std::string GenerateLicenseFilename(const std::string& content_id,
const std::string& provider_id) {
std::string data(content_id + provider_id);
std::string hash;
hash.resize(SHA256_DIGEST_LENGTH);
const unsigned char* input =
reinterpret_cast<const unsigned char*>(data.data());
unsigned char* output = reinterpret_cast<unsigned char*>(&hash[0]);
SHA256(input, data.size(), output);
return std::string(std::string(kBasePathPrefix) + wvutil::b2a_hex(hash) +
std::string(kLicenseFileNameSuffix));
}
} // namespace
namespace wvcas {
class MediaContext : public CasMediaId {
public:
MediaContext() : CasMediaId() {}
~MediaContext() override {}
MediaContext(const MediaContext&) = delete;
MediaContext& operator=(const MediaContext&) = delete;
const std::string content_id() override { return pssh_.content_id(); }
const std::string provider_id() override { return pssh_.provider(); }
CasStatus initialize(const std::string& init_data) override {
if (!pssh_.ParseFromString(init_data)) {
return CasStatus(CasStatusCode::kInvalidParameter, "invalid init_data");
}
return CasStatusCode::kNoError;
}
private:
video_widevine::WidevinePsshData pssh_;
};
std::unique_ptr<CasMediaId> CasMediaId::create() {
std::unique_ptr<MediaContext> ctx = make_unique<MediaContext>();
return std::move(ctx);
}
std::shared_ptr<CryptoSession> WidevineCas::getCryptoSession() {
return std::make_shared<CryptoSession>();
}
std::unique_ptr<CasLicense> WidevineCas::getCasLicense() {
return make_unique<CasLicense>();
}
std::unique_ptr<wvutil::FileSystem> WidevineCas::getFileSystem() {
return make_unique<wvutil::FileSystem>();
}
std::shared_ptr<WidevineCasSession> WidevineCas::newCasSession() {
return std::make_shared<WidevineCasSession>();
}
void WidevineCas::OnTimerEvent() {
std::unique_lock<std::mutex> locker(lock_);
if (cas_license_.get() != nullptr) {
cas_license_->OnTimerEvent();
// Delete expired license after firing expired event in policy_engine
if (cas_license_->IsExpired() && (media_id_.get() != nullptr)) {
std::string filename = GenerateLicenseFilename(media_id_->content_id(),
media_id_->provider_id());
if (!file_system_->Exists(filename)) {
LOGI("No expired license file stored in disk");
} else {
if (RemoveFile(*file_system_, filename)) {
LOGI("Remove expired license file from disk successfully.");
}
}
}
}
}
CasStatus WidevineCas::initialize(CasEventListener* event_listener) {
std::unique_lock<std::mutex> locker(lock_);
crypto_session_ = getCryptoSession();
// For session name generation.
srand(time(nullptr));
// Setup an oemcrypto session.
CasStatus status = crypto_session_->initialize();
if (!status.ok()) {
LOGE("WidevineCas initialization failed: %d", status.status_code());
return status;
}
file_system_ = getFileSystem();
cas_license_ = getCasLicense();
status = cas_license_->initialize(crypto_session_, event_listener);
if (!status.ok()) {
LOGE("WidevineCas initialization failed: %d", status.status_code());
return status;
}
std::string cert_filename_path(std::string(kBasePathPrefix) +
std::string(kCertFileBase));
// Try to read a certificate if one exists. If any error occurs, just ignore
// it and let new cert file overwrite the existing file.
std::string cert_file;
if (ReadFileFromStorage(*file_system_, cert_filename_path, &cert_file)) {
LOGI("read cert.bin successfully");
if (!HandleStoredDrmCert(cert_file).ok()) {
return CasStatusCode::kNoError;
}
}
event_listener_ = event_listener;
return CasStatusCode::kNoError;
}
// TODO(jfore): Split out the functionality and move the callback out of this
// class.
CasStatus WidevineCas::openSession(WvCasSessionId* sessionId) {
std::unique_lock<std::mutex> locker(lock_);
if (nullptr == sessionId) {
return CasStatus(CasStatusCode::kInvalidParameter,
"missing openSession sessionId");
}
CasSessionPtr session = newCasSession();
CasStatus status = session->initialize(
crypto_session_, reinterpret_cast<uint32_t*>(sessionId));
if (CasStatusCode::kNoError != status.status_code()) {
return status;
}
WidevineCasSessionMap::instance().AddSession(*sessionId, session);
return CasStatusCode::kNoError;
}
CasStatus WidevineCas::closeSession(WvCasSessionId sessionId) {
std::unique_lock<std::mutex> locker(lock_);
CasSessionPtr session =
WidevineCasSessionMap::instance().GetSession(sessionId);
// TODO(jfore): Add a log event if the session doesn't exist and perhaps raise
// an error.`
if (session == nullptr) {
return CasStatus(CasStatusCode::kSessionNotFound, "unknown session id");
}
WidevineCasSessionMap::instance().RemoveSession(sessionId);
return CasStatusCode::kNoError;
}
// TODO(jfore): Add unit test to widevine_cas_api_test.cpp that is added in
// another cl.
CasStatus WidevineCas::processEcm(WvCasSessionId sessionId, const CasEcm& ecm) {
LOGD("WidevineCasPlugin::processEcm");
std::unique_lock<std::mutex> locker(lock_);
// If we don't have a license yet, save the ecm and session id.
if (!has_license_) {
deferred_ecms_.emplace(sessionId, ecm);
return CasStatusCode::kDeferedEcmProcessing;
}
return HandleProcessEcm(sessionId, ecm);
}
CasStatus WidevineCas::HandleProcessEcm(const WvCasSessionId& sessionId,
const CasEcm& ecm) {
if (cas_license_->IsExpired()) {
return CasStatus(CasStatusCode::kCasLicenseError,
"license is expired, unable to process ecm");
}
CasSessionPtr session =
WidevineCasSessionMap::instance().GetSession(sessionId);
if (session == nullptr) {
return CasStatus(CasStatusCode::kSessionNotFound,
"unknown session for processEcm");
}
uint8_t ecm_age_previous = session->GetEcmAgeRestriction();
CasStatus status = session->processEcm(ecm, parental_control_age_);
uint8_t ecm_age_current = session->GetEcmAgeRestriction();
if (event_listener_ != nullptr && ecm_age_current != ecm_age_previous) {
event_listener_->OnAgeRestrictionUpdated(sessionId, ecm_age_current);
}
if (status.ok()) {
cas_license_->BeginDecryption();
}
return status;
}
CasStatus WidevineCas::HandleDeferredECMs() {
for (const auto& deferred_ecm : deferred_ecms_) {
CasStatus status =
HandleProcessEcm(deferred_ecm.first, deferred_ecm.second);
if (!status.ok()) {
return status;
}
}
deferred_ecms_.clear();
return CasStatusCode::kNoError;
}
CasStatus WidevineCas::generateDeviceProvisioningRequest(
std::string* provisioning_request) {
std::unique_lock<std::mutex> locker(lock_);
if (provisioning_request == nullptr) {
return CasStatus(CasStatusCode::kInvalidParameter,
"missing output buffer for provisioning request");
}
return cas_license_->GenerateDeviceProvisioningRequest(provisioning_request);
}
CasStatus WidevineCas::handleProvisioningResponse(const std::string& response) {
if (response.empty()) {
return CasStatus(CasStatusCode::kCasLicenseError,
"empty individualization response");
}
std::string device_file;
std::unique_lock<std::mutex> locker(lock_);
CasStatus status = cas_license_->HandleDeviceProvisioningResponse(
response, &device_certificate_, &wrapped_rsa_key_, &device_file);
if (!status.ok()) {
return status;
}
if (!device_file.empty()) {
std::string cert_filename(std::string(kBasePathPrefix) +
std::string(kCertFileBase));
StoreFile(*file_system_, cert_filename, device_file);
}
return CasStatusCode::kNoError;
}
CasStatus WidevineCas::generateEntitlementRequest(
const std::string& init_data, std::string* entitlement_request,
std::string& license_id) {
media_id_ = CasMediaId::create();
CasStatus status = media_id_->initialize(init_data);
if (!status.ok()) {
return status;
}
std::string license_file;
std::string filename = GenerateLicenseFilename(media_id_->content_id(),
media_id_->provider_id());
if (ReadFileFromStorage(*file_system_, filename, &license_file)) {
CasStatus status =
cas_license_->HandleStoredLicense(wrapped_rsa_key_, license_file);
if (status.ok()) {
// If license file is expired, don't proceed the request. Also
// delete the stored license file.
std::unique_lock<std::mutex> locker(lock_);
if (cas_license_->IsExpired()) {
if (!RemoveFile(*file_system_, filename)) {
return CasStatus(CasStatusCode::kInvalidLicenseFile,
"unable to remove expired license file from disk");
}
LOGI("Remove expired license file from disk successfully.");
return CasStatus(CasStatusCode::kCasLicenseError,
"license is expired, unable to process emm");
}
license_id =
filename.substr(0, filename.size() - strlen(kLicenseFileNameSuffix));
policy_timer_.Start(this, 1);
has_license_ = true;
return HandleDeferredECMs();
}
LOGI("Fallthru");
}
if (entitlement_request == nullptr) {
return CasStatus(CasStatusCode::kInvalidParameter,
"missing output buffer for entitlement request");
}
std::unique_lock<std::mutex> locker(lock_);
return cas_license_->GenerateEntitlementRequest(
init_data, device_certificate_, wrapped_rsa_key_, license_type_,
entitlement_request);
}
CasStatus WidevineCas::handleEntitlementResponse(const std::string& response,
std::string& license_id) {
if (response.empty()) {
return CasStatus(CasStatusCode::kCasLicenseError,
"empty entitlement response");
}
std::string device_file;
std::unique_lock<std::mutex> locker(lock_);
CasStatus status =
cas_license_->HandleEntitlementResponse(response, &device_file);
if (status.ok()) {
// A license has been successfully loaded. Load any ecms that may have been
// deferred waiting for the license.
has_license_ = true;
status = HandleDeferredECMs();
if (!status.ok()) {
return status;
}
policy_timer_.Start(this, 1);
if (device_file.empty()) {
return status;
}
if (!device_file.empty()) {
std::string filename = GenerateLicenseFilename(media_id_->content_id(),
media_id_->provider_id());
StoreFile(*file_system_, filename, device_file);
// license_id will be the filename without ".lic" extension.
license_id =
filename.substr(0, filename.size() - strlen(kLicenseFileNameSuffix));
}
}
return status;
}
CasStatus WidevineCas::generateEntitlementRenewalRequest(
std::string* entitlement_renewal_request) {
if (entitlement_renewal_request == nullptr) {
return CasStatus(CasStatusCode::kInvalidParameter,
"missing output buffer for entitlement renewal request");
}
return cas_license_->GenerateEntitlementRenewalRequest(
device_certificate_, entitlement_renewal_request);
}
CasStatus WidevineCas::handleEntitlementRenewalResponse(
const std::string& response, std::string& license_id) {
if (response.empty()) {
return CasStatus(CasStatusCode::kCasLicenseError,
"empty entitlement renewal response");
}
std::string device_file;
std::unique_lock<std::mutex> locker(lock_);
CasStatus status =
cas_license_->HandleEntitlementRenewalResponse(response, &device_file);
if (!status.ok()) {
return status;
}
if (!device_file.empty()) {
std::string filename = GenerateLicenseFilename(media_id_->content_id(),
media_id_->provider_id());
StoreFile(*file_system_, filename, device_file);
// license_id will be the filename without ".lic" extension.
license_id =
filename.substr(0, filename.size() - std::string(".lic").size());
}
return CasStatusCode::kNoError;
}
CasStatus WidevineCas::RemoveLicense(const std::string file_name) {
// Check if the license is in use. If it is, besides removing the license,
// update policy in current license. Else, we just directly remove it.
if (nullptr == media_id_.get()) {
return CasStatus(CasStatusCode::kCasLicenseError, "No media id");
}
// Remove the license file given the file_name user provides.
if (!RemoveFile(*file_system_, file_name)) {
return CasStatus(CasStatusCode::kInvalidLicenseFile,
"unable to remove license file from disk");
}
LOGI("Remove license file from disk successfully.");
std::string used_license_filename = GenerateLicenseFilename(
media_id_->content_id(), media_id_->provider_id());
if (file_name.compare(used_license_filename) == 0) {
// Update license policy for the in-used license. Plugin will not allowed to
// play stream, store and renew license unless a new plugin instance is
// created.
std::unique_lock<std::mutex> locker(lock_);
cas_license_->UpdateLicenseForLicenseRemove();
}
return CasStatusCode::kNoError;
}
bool WidevineCas::is_provisioned() const {
return (!(device_certificate_.empty() || wrapped_rsa_key_.empty()));
}
CasStatus WidevineCas::ProcessCAPrivateData(const CasData& private_data,
std::string* init_data) {
if (init_data == nullptr) {
return CasStatus(CasStatusCode::kInvalidParameter,
"missing output buffer for init_data");
}
// Parse provider and content id from CA descriptor.
video_widevine::CaDescriptorPrivateData descriptor;
descriptor.ParseFromArray(private_data.data(), private_data.size());
if (!descriptor.has_content_id() || !descriptor.has_provider()) {
return CasStatus(CasStatusCode::kInvalidParameter,
"unable to parse private data");
}
// Build PSSH of type ENTITLEMENT.
video_widevine::WidevinePsshData pssh;
pssh.set_provider(descriptor.provider());
pssh.set_content_id(descriptor.content_id());
pssh.set_type(video_widevine::WidevinePsshData::ENTITLEMENT);
pssh.SerializeToString(init_data);
return CasStatusCode::kNoError;
}
CasStatus WidevineCas::ProcessSessionCAPrivateData(WvCasSessionId session_id,
const CasData& private_data,
std::string* init_data) {
if (!WidevineCasSessionMap::instance().GetSession(session_id)) {
return CasStatus(CasStatusCode::kCasLicenseError, "invalid session id");
}
return ProcessCAPrivateData(private_data, init_data);
}
CasStatus WidevineCas::GetUniqueID(std::string* buffer) {
return crypto_session_->GetDeviceID(buffer);
}
CasStatus WidevineCas::HandleStoredDrmCert(const std::string& certificate) {
if (certificate.empty()) {
return CasStatus(CasStatusCode::kCasLicenseError, "empty certificate data");
}
CasStatus status = cas_license_->HandleStoredDrmCert(
certificate, &device_certificate_, &wrapped_rsa_key_);
return status;
}
CasStatus WidevineCas::HandleSetParentalControlAge(const CasData& data) {
if (data.empty()) {
return CasStatus(CasStatusCode::kCasLicenseError,
"missing value of parental control min age");
}
parental_control_age_ = data[0];
LOGI("Parental control age set to: ", parental_control_age_);
return CasStatusCode::kNoError;
}
} // namespace wvcas