Widevine MediaCas client code that works with Android R

This commit is contained in:
Lu Chen
2020-08-13 15:18:12 -07:00
parent ff9728aaa2
commit 0f6db6f751
243 changed files with 47012 additions and 0 deletions

View File

@@ -0,0 +1,535 @@
#include <openssl/sha.h>
#include "ca_descriptor.pb.h"
#include "cas_events.h"
#include "cas_util.h"
#include "license_protocol.pb.h"
#include "log.h"
#include "string_conversions.h"
#include "widevine_cas_api.h"
#include "widevine_cas_session_map.h"
static constexpr char kBasePathPrefix[] = "/data/vendor/mediacas/IDM/widevine/";
static constexpr char kCertFileBase[] = "cert.bin";
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& id_data_from_media_id,
const std::string& provider_id) {
std::string data(id_data_from_media_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(".lic"));
}
} // 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(); }
const int group_ids_size() override { return pssh_.group_ids_size(); }
const std::string group_id() override {
if (group_ids_size() > 0) {
return pssh_.group_ids(0);
}
return "";
}
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;
if (media_id_->group_ids_size() > 0) {
filename = GenerateLicenseFilename(media_id_->group_id(),
media_id_->provider_id());
} else {
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) {
media_id_ = CasMediaId::create();
CasStatus status = media_id_->initialize(init_data);
if (!status.ok()) {
return status;
}
std::string license_file;
std::string filename;
if (media_id_->group_ids_size() > 0) {
filename = GenerateLicenseFilename(media_id_->group_id(),
media_id_->provider_id());
} else {
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");
}
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;
if (media_id_->group_ids_size() > 0) {
filename = GenerateLicenseFilename(media_id_->group_id(),
media_id_->provider_id());
} else {
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 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;
if (media_id_->group_ids_size() > 0) {
filename = GenerateLicenseFilename(media_id_->group_id(),
media_id_->provider_id());
} else {
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;
if (media_id_->group_ids_size() > 0) {
used_license_filename = GenerateLicenseFilename(media_id_->group_id(),
media_id_->provider_id());
} else {
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, content id and group 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());
// group id is optional in ca_descriptor.
if (descriptor.has_group_id()) {
pssh.add_group_ids(descriptor.group_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