(This is a merge of http://go/wvgerrit/94928.) In OEMCrypto v16, we dropped support for 'cens' and 'cbc1'. However, we did not redefine the pattern (0,0) to be a valid pattern for 'cbcs', even though it was no longer being used to signal 'cbc1'. Instead, we made the CDM reject CTR with a pattern ('cens') and CBC with a (0,0) pattern ('cbc1') to mirror the behavior of OEMCrypto v16. However, some apps have been using 'cbc1' mode to decrypt audio in 'cbcs' content. This is normally not possible but is possible for a subset of content. Furthermore, it is easy to do by accident because of the way most packagers package 'cbcs' audio and the special significance Widevine has historically given the (0,0) pattern. This patch updates the CDM to not reject CBC with a (0,0) pattern but instead treat it as 'cbcs' content. To decrypt it correctly, the pattern is treated specially inside the CDM core and converted to the recommended equivalent pattern — (10,0) — before passing the content to OEMCrypto. For more specifics, please see the design doc: http://go/vclfg Bug: 150219982 Test: ExoPlayer Demo App 'cbcs' Content Test: GTS 'cbcs' Content Change-Id: I334ff15db5f7b7d62040a036ba6d17515c3caee4
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 Master
|
|
// 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
|