[ Merge of http://go/wvgerrit/108084 ] The Widevine License Agreement has been renamed to use inclusive language. This covers files in the android directory. Bug: 168562298 Test: verified compilation (comment only change) Change-Id: I0f9e6445e0168ebe85425baeb81371e182e5a39c
262 lines
8.6 KiB
C++
262 lines
8.6 KiB
C++
//
|
|
// Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary
|
|
// source code may only be used and distributed under the Widevine License
|
|
// Agreement.
|
|
//
|
|
|
|
//#define LOG_NDEBUG 0
|
|
#define LOG_TAG "WVCdm"
|
|
#include <utils/Log.h>
|
|
|
|
#include "WVCryptoPlugin.h"
|
|
|
|
#include <algorithm>
|
|
#include <endian.h>
|
|
#include <iterator>
|
|
#include <string.h>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "mapErrors-inl.h"
|
|
#include "media/stagefright/MediaErrors.h"
|
|
#include "OEMCryptoCENC.h"
|
|
#include "openssl/sha.h"
|
|
#include "utils/Errors.h"
|
|
#include "utils/String8.h"
|
|
#include "wv_cdm_constants.h"
|
|
#include "WVErrors.h"
|
|
|
|
namespace wvdrm {
|
|
|
|
using namespace android;
|
|
using namespace std;
|
|
using namespace wvcdm;
|
|
|
|
WVCryptoPlugin::WVCryptoPlugin(const void* data, size_t size,
|
|
const sp<WvContentDecryptionModule>& cdm)
|
|
: mCDM(cdm),
|
|
mTestMode(false),
|
|
mSessionId(configureTestMode(data, size)) {}
|
|
|
|
CdmSessionId WVCryptoPlugin::configureTestMode(const void* data, size_t size) {
|
|
CdmSessionId sessionId;
|
|
if (data != NULL) {
|
|
sessionId.assign(static_cast<const char *>(data), size);
|
|
size_t index = sessionId.find("test_mode");
|
|
if (index != string::npos) {
|
|
sessionId = sessionId.substr(0, index);
|
|
mTestMode = true;
|
|
}
|
|
}
|
|
if (!mCDM->IsOpenSession(sessionId)) {
|
|
sessionId.clear();
|
|
}
|
|
return sessionId;
|
|
}
|
|
|
|
bool WVCryptoPlugin::requiresSecureDecoderComponent(const char* mime) const {
|
|
if (!strncasecmp(mime, "video/", 6)) {
|
|
// Type is video, so query CDM to see if we require a secure decoder.
|
|
CdmQueryMap status;
|
|
|
|
CdmResponseType res = mCDM->QuerySessionStatus(mSessionId, &status);
|
|
|
|
if (!isCdmResponseTypeSuccess(res)) {
|
|
ALOGE("Error querying CDM status: %u", res);
|
|
return false;
|
|
}
|
|
|
|
return status[QUERY_KEY_SECURITY_LEVEL] == QUERY_VALUE_SECURITY_LEVEL_L1;
|
|
} else {
|
|
// Type is not video, so never require a secure decoder.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void WVCryptoPlugin::notifyResolution(uint32_t width, uint32_t height) {
|
|
mCDM->NotifyResolution(mSessionId, width, height);
|
|
}
|
|
|
|
status_t WVCryptoPlugin::setMediaDrmSession(const Vector<uint8_t>& sessionId) {
|
|
CdmSessionId cdmSessionId(reinterpret_cast<const char *>(sessionId.array()),
|
|
sessionId.size());
|
|
if (sessionId.size() == 0) {
|
|
return android::BAD_VALUE;
|
|
}
|
|
if (!mCDM->IsOpenSession(cdmSessionId)) {
|
|
return android::ERROR_DRM_SESSION_NOT_OPENED;
|
|
} else {
|
|
mSessionId = cdmSessionId;
|
|
return android::NO_ERROR;
|
|
}
|
|
}
|
|
|
|
// Returns negative values for error code and positive values for the size of
|
|
// decrypted data. In theory, the output size can be larger than the input
|
|
// size, but in practice this should never happen for AES-CTR.
|
|
ssize_t WVCryptoPlugin::decrypt(bool secure, const uint8_t key[KEY_ID_SIZE],
|
|
const uint8_t iv[KEY_IV_SIZE], Mode mode,
|
|
const Pattern& pattern, const void* srcPtr,
|
|
const SubSample* subSamples,
|
|
size_t numSubSamples, void* dstPtr,
|
|
AString* errorDetailMsg) {
|
|
if (mode != kMode_Unencrypted &&
|
|
mode != kMode_AES_CTR &&
|
|
mode != kMode_AES_CBC) {
|
|
errorDetailMsg->setTo(
|
|
"The requested encryption mode is not supported by Widevine CDM.");
|
|
return kErrorUnsupportedCrypto;
|
|
} else if (mode == kMode_AES_CTR &&
|
|
(pattern.mEncryptBlocks != 0 || pattern.mSkipBlocks != 0)) {
|
|
errorDetailMsg->setTo(
|
|
"The 'cens' schema is not supported by Widevine CDM.");
|
|
return kErrorUnsupportedCrypto;
|
|
}
|
|
|
|
// Convert parameters to the form the CDM wishes to consume them in.
|
|
const KeyId keyId(reinterpret_cast<const char*>(key), KEY_ID_SIZE);
|
|
vector<uint8_t> ivVector(iv, iv + KEY_IV_SIZE);
|
|
const uint8_t* const source = static_cast<const uint8_t*>(srcPtr);
|
|
uint8_t* const dest = static_cast<uint8_t*>(dstPtr);
|
|
|
|
// Set up the decrypt params
|
|
CdmDecryptionParametersV16 params;
|
|
params.key_id = keyId;
|
|
params.is_secure = secure;
|
|
if (mode == kMode_AES_CTR) {
|
|
params.cipher_mode = kCipherModeCtr;
|
|
} else if (mode == kMode_AES_CBC) {
|
|
params.cipher_mode = kCipherModeCbc;
|
|
}
|
|
params.pattern.encrypt_blocks = pattern.mEncryptBlocks;
|
|
params.pattern.skip_blocks = pattern.mSkipBlocks;
|
|
|
|
// Set up the sample
|
|
// Android's API only supports one at a time
|
|
params.samples.emplace_back();
|
|
CdmDecryptionSample& sample = params.samples.back();
|
|
sample.encrypt_buffer = source;
|
|
sample.decrypt_buffer = dest;
|
|
sample.decrypt_buffer_offset = 0;
|
|
sample.iv = ivVector;
|
|
|
|
// Set up the subsamples
|
|
// We abuse std::transform() here to also do some side-effects: Tallying the
|
|
// total size of the sample and checking if any of the data is protected.
|
|
size_t totalSize = 0;
|
|
bool hasProtectedData = false;
|
|
sample.subsamples.reserve(numSubSamples);
|
|
std::transform(subSamples, subSamples + numSubSamples,
|
|
std::back_inserter(sample.subsamples),
|
|
[&](const SubSample& subSample) -> CdmDecryptionSubsample {
|
|
totalSize +=
|
|
subSample.mNumBytesOfClearData + subSample.mNumBytesOfEncryptedData;
|
|
hasProtectedData |= subSample.mNumBytesOfEncryptedData > 0;
|
|
return CdmDecryptionSubsample(subSample.mNumBytesOfClearData,
|
|
subSample.mNumBytesOfEncryptedData);
|
|
});
|
|
|
|
sample.encrypt_buffer_length = totalSize;
|
|
sample.decrypt_buffer_size = totalSize;
|
|
|
|
if (mode == kMode_Unencrypted && hasProtectedData) {
|
|
errorDetailMsg->setTo("Protected ranges found in allegedly clear data.");
|
|
return kErrorExpectedUnencrypted;
|
|
}
|
|
|
|
// Decrypt
|
|
status_t res = attemptDecrypt(params, hasProtectedData, errorDetailMsg);
|
|
if (res != android::OK) {
|
|
return res;
|
|
}
|
|
|
|
// In test mode, we return an error that causes a detailed error
|
|
// message string containing a SHA256 hash of the decrypted data
|
|
// to get passed to the java app via CryptoException. The test app
|
|
// can then use the hash to verify that decryption was successful.
|
|
|
|
if (mTestMode) {
|
|
if (secure) {
|
|
// can't access data in secure mode
|
|
errorDetailMsg->setTo("secure");
|
|
} else {
|
|
SHA256_CTX ctx;
|
|
uint8_t digest[SHA256_DIGEST_LENGTH];
|
|
SHA256_Init(&ctx);
|
|
SHA256_Update(&ctx, dstPtr, totalSize);
|
|
SHA256_Final(digest, &ctx);
|
|
String8 buf;
|
|
for (size_t i = 0; i < sizeof(digest); i++) {
|
|
buf.appendFormat("%02x", digest[i]);
|
|
}
|
|
errorDetailMsg->setTo(buf.string());
|
|
}
|
|
|
|
return kErrorTestMode;
|
|
}
|
|
|
|
return static_cast<ssize_t>(totalSize);
|
|
}
|
|
|
|
status_t WVCryptoPlugin::attemptDecrypt(
|
|
const CdmDecryptionParametersV16& params, bool hasProtectedData,
|
|
AString* errorDetailMsg) {
|
|
CdmResponseType res = mCDM->DecryptV16(mSessionId, hasProtectedData,
|
|
params);
|
|
|
|
if (isCdmResponseTypeSuccess(res)) {
|
|
return android::OK;
|
|
} else {
|
|
ALOGE("Decrypt error in session %s during a sample %s protected data: %d",
|
|
mSessionId.c_str(),
|
|
hasProtectedData ? "with" : "without",
|
|
res);
|
|
bool actionableError = true;
|
|
switch (res) {
|
|
case wvcdm::INSUFFICIENT_CRYPTO_RESOURCES:
|
|
errorDetailMsg->setTo(
|
|
"Error decrypting data: insufficient crypto resources");
|
|
break;
|
|
case wvcdm::NEED_KEY:
|
|
case wvcdm::KEY_NOT_FOUND_IN_SESSION:
|
|
errorDetailMsg->setTo(
|
|
"Error decrypting data: requested key has not been loaded");
|
|
break;
|
|
case wvcdm::DECRYPT_NOT_READY:
|
|
errorDetailMsg->setTo(
|
|
"Error decrypting data: license validity period is in the future");
|
|
break;
|
|
case wvcdm::SESSION_NOT_FOUND_FOR_DECRYPT:
|
|
errorDetailMsg->setTo(
|
|
"Error decrypting data: session not found, possibly reclaimed");
|
|
break;
|
|
case wvcdm::DECRYPT_ERROR:
|
|
errorDetailMsg->setTo(
|
|
"Error decrypting data: unspecified error");
|
|
break;
|
|
case wvcdm::INSUFFICIENT_OUTPUT_PROTECTION:
|
|
case wvcdm::ANALOG_OUTPUT_ERROR:
|
|
errorDetailMsg->setTo(
|
|
"Error decrypting data: insufficient output protection");
|
|
break;
|
|
case wvcdm::KEY_PROHIBITED_FOR_SECURITY_LEVEL:
|
|
errorDetailMsg->setTo(
|
|
"Error decrypting data: key prohibited for security level");
|
|
break;
|
|
default:
|
|
actionableError = false;
|
|
break;
|
|
}
|
|
|
|
if (actionableError) {
|
|
// This error is actionable by the app and should be passed up.
|
|
return mapCdmResponseType(res);
|
|
} else {
|
|
// Swallow the specifics of other errors to obscure decrypt internals.
|
|
return kErrorCDMGeneric;
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace wvdrm
|