| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| //#define LOG_NDEBUG 0 |
| #define LOG_TAG "ClearKeyCasPlugin" |
| |
| #include "ClearKeyFetcher.h" |
| #include "ecm.h" |
| #include "ClearKeyLicenseFetcher.h" |
| #include "ClearKeyCasPlugin.h" |
| #include "ClearKeySessionLibrary.h" |
| #include <media/stagefright/foundation/ABuffer.h> |
| #include <media/stagefright/foundation/ADebug.h> |
| #include <media/stagefright/foundation/hexdump.h> |
| #include <media/stagefright/MediaErrors.h> |
| #include <utils/Log.h> |
| |
| android::CasFactory* createCasFactory() { |
| return new android::clearkeycas::ClearKeyCasFactory(); |
| } |
| |
| android::DescramblerFactory *createDescramblerFactory() |
| { |
| return new android::clearkeycas::ClearKeyDescramblerFactory(); |
| } |
| |
| namespace android { |
| namespace clearkeycas { |
| |
| static const int32_t sClearKeySystemId = 0xF6D8; |
| |
| bool ClearKeyCasFactory::isSystemIdSupported(int32_t CA_system_id) const { |
| return CA_system_id == sClearKeySystemId; |
| } |
| |
| status_t ClearKeyCasFactory::queryPlugins( |
| std::vector<CasPluginDescriptor> *descriptors) const { |
| descriptors->clear(); |
| descriptors->push_back({sClearKeySystemId, String8("Clear Key CAS")}); |
| return OK; |
| } |
| |
| status_t ClearKeyCasFactory::createPlugin( |
| int32_t CA_system_id, |
| void *appData, |
| CasPluginCallback callback, |
| CasPlugin **plugin) { |
| if (!isSystemIdSupported(CA_system_id)) { |
| return BAD_VALUE; |
| } |
| |
| *plugin = new ClearKeyCasPlugin(appData, callback); |
| return OK; |
| } |
| |
| status_t ClearKeyCasFactory::createPlugin( |
| int32_t CA_system_id, |
| void *appData, |
| CasPluginCallbackExt callback, |
| CasPlugin **plugin) { |
| if (!isSystemIdSupported(CA_system_id)) { |
| return BAD_VALUE; |
| } |
| |
| *plugin = new ClearKeyCasPlugin(appData, callback); |
| return OK; |
| } |
| //////////////////////////////////////////////////////////////////////////////// |
| bool ClearKeyDescramblerFactory::isSystemIdSupported( |
| int32_t CA_system_id) const { |
| return CA_system_id == sClearKeySystemId; |
| } |
| |
| status_t ClearKeyDescramblerFactory::createPlugin( |
| int32_t CA_system_id, DescramblerPlugin** plugin) { |
| if (!isSystemIdSupported(CA_system_id)) { |
| return BAD_VALUE; |
| } |
| |
| *plugin = new ClearKeyDescramblerPlugin(); |
| return OK; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| ClearKeyCasPlugin::ClearKeyCasPlugin( |
| void *appData, CasPluginCallback callback) |
| : mCallback(callback), mCallbackExt(NULL), mStatusCallback(NULL), |
| mAppData(appData) { |
| ALOGV("CTOR"); |
| } |
| |
| ClearKeyCasPlugin::ClearKeyCasPlugin( |
| void *appData, CasPluginCallbackExt callback) |
| : mCallback(NULL), mCallbackExt(callback), mAppData(appData) { |
| ALOGV("CTOR"); |
| } |
| |
| ClearKeyCasPlugin::~ClearKeyCasPlugin() { |
| ALOGV("DTOR"); |
| ClearKeySessionLibrary::get()->destroyPlugin(this); |
| } |
| |
| status_t ClearKeyCasPlugin::setStatusCallback( |
| CasPluginStatusCallback callback) { |
| ALOGV("setStatusCallback"); |
| mStatusCallback = callback; |
| return OK; |
| } |
| |
| status_t ClearKeyCasPlugin::setPrivateData(const CasData &/*data*/) { |
| ALOGV("setPrivateData"); |
| |
| return OK; |
| } |
| |
| static String8 sessionIdToString(const std::vector<uint8_t> &array) { |
| String8 result; |
| for (size_t i = 0; i < array.size(); i++) { |
| result.appendFormat("%02x ", array[i]); |
| } |
| if (result.empty()) { |
| result.append("(null)"); |
| } |
| return result; |
| } |
| |
| status_t ClearKeyCasPlugin::openSession(CasSessionId* sessionId) { |
| ALOGV("openSession"); |
| |
| return ClearKeySessionLibrary::get()->addSession(this, sessionId); |
| } |
| |
| status_t ClearKeyCasPlugin::openSession(uint32_t intent, uint32_t mode, |
| CasSessionId* sessionId) { |
| ALOGV("openSession with intent=%d, mode=%d", intent, mode); |
| // Echo the received information to the callback. |
| // Clear key plugin doesn't use any event, echo'ing for testing only. |
| if (mStatusCallback != NULL) { |
| mStatusCallback((void*)mAppData, intent, mode); |
| } |
| |
| // Clear key plugin doesn't use intent and mode. |
| return ClearKeySessionLibrary::get()->addSession(this, sessionId); |
| } |
| |
| status_t ClearKeyCasPlugin::closeSession(const CasSessionId &sessionId) { |
| ALOGV("closeSession: sessionId=%s", sessionIdToString(sessionId).c_str()); |
| std::shared_ptr<ClearKeyCasSession> session = |
| ClearKeySessionLibrary::get()->findSession(sessionId); |
| if (session.get() == nullptr) { |
| return ERROR_CAS_SESSION_NOT_OPENED; |
| } |
| |
| ClearKeySessionLibrary::get()->destroySession(sessionId); |
| return OK; |
| } |
| |
| status_t ClearKeyCasPlugin::setSessionPrivateData( |
| const CasSessionId &sessionId, const CasData & /*data*/) { |
| ALOGV("setSessionPrivateData: sessionId=%s", |
| sessionIdToString(sessionId).c_str()); |
| std::shared_ptr<ClearKeyCasSession> session = |
| ClearKeySessionLibrary::get()->findSession(sessionId); |
| if (session.get() == nullptr) { |
| return ERROR_CAS_SESSION_NOT_OPENED; |
| } |
| return OK; |
| } |
| |
| status_t ClearKeyCasPlugin::processEcm( |
| const CasSessionId &sessionId, const CasEcm& ecm) { |
| ALOGV("processEcm: sessionId=%s", sessionIdToString(sessionId).c_str()); |
| std::shared_ptr<ClearKeyCasSession> session = |
| ClearKeySessionLibrary::get()->findSession(sessionId); |
| if (session.get() == nullptr) { |
| return ERROR_CAS_SESSION_NOT_OPENED; |
| } |
| |
| Mutex::Autolock lock(mKeyFetcherLock); |
| |
| return session->updateECM(mKeyFetcher.get(), (void*)ecm.data(), ecm.size()); |
| } |
| |
| status_t ClearKeyCasPlugin::processEmm(const CasEmm& /*emm*/) { |
| ALOGV("processEmm"); |
| Mutex::Autolock lock(mKeyFetcherLock); |
| |
| return OK; |
| } |
| |
| status_t ClearKeyCasPlugin::sendEvent( |
| int32_t event, int32_t arg, const CasData &eventData) { |
| ALOGV("sendEvent: event=%d, arg=%d", event, arg); |
| // Echo the received event to the callback. |
| // Clear key plugin doesn't use any event, echo'ing for testing only. |
| if (mCallback != NULL) { |
| mCallback((void*)mAppData, event, arg, (uint8_t*)eventData.data(), |
| eventData.size()); |
| } else if (mCallbackExt != NULL) { |
| mCallbackExt((void*)mAppData, event, arg, (uint8_t*)eventData.data(), |
| eventData.size(), NULL); |
| } |
| return OK; |
| } |
| |
| status_t ClearKeyCasPlugin::sendSessionEvent( |
| const CasSessionId &sessionId, int32_t event, |
| int arg, const CasData &eventData) { |
| ALOGV("sendSessionEvent: sessionId=%s, event=%d, arg=%d", |
| sessionIdToString(sessionId).c_str(), event, arg); |
| // Echo the received event to the callback. |
| // Clear key plugin doesn't use any event, echo'ing for testing only. |
| if (mCallbackExt != NULL) { |
| mCallbackExt((void*)mAppData, event, arg, (uint8_t*)eventData.data(), |
| eventData.size(), &sessionId); |
| } |
| |
| return OK; |
| } |
| |
| status_t ClearKeyCasPlugin::provision(const String8 &str) { |
| ALOGV("provision: provisionString=%s", str.c_str()); |
| Mutex::Autolock lock(mKeyFetcherLock); |
| |
| std::unique_ptr<ClearKeyLicenseFetcher> license_fetcher; |
| license_fetcher.reset(new ClearKeyLicenseFetcher()); |
| status_t err = license_fetcher->Init(str.c_str()); |
| if (err != OK) { |
| ALOGE("provision: failed to init ClearKeyLicenseFetcher (err=%d)", err); |
| return err; |
| } |
| |
| std::unique_ptr<ClearKeyFetcher> key_fetcher; |
| key_fetcher.reset(new ClearKeyFetcher(std::move(license_fetcher))); |
| err = key_fetcher->Init(); |
| if (err != OK) { |
| ALOGE("provision: failed to init ClearKeyFetcher (err=%d)", err); |
| return err; |
| } |
| |
| ALOGV("provision: using ClearKeyFetcher"); |
| mKeyFetcher = std::move(key_fetcher); |
| |
| return OK; |
| } |
| |
| status_t ClearKeyCasPlugin::refreshEntitlements( |
| int32_t refreshType, const CasData &/*refreshData*/) { |
| ALOGV("refreshEntitlements: refreshType=%d", refreshType); |
| Mutex::Autolock lock(mKeyFetcherLock); |
| |
| return OK; |
| } |
| |
| /////////////////////////////////////////////////////////////////////// |
| |
| // AES-128 CBC-CTS decrypt optimized for Transport Packets. |key| is the AES |
| // key (odd key or even key), |length| is the data size, and |buffer| is the |
| // ciphertext to be decrypted in place. |
| status_t TpBlockCtsDecrypt(const AES_KEY& key, size_t length, char* buffer) { |
| CHECK(buffer); |
| |
| // Invariant: Packet must be at least 16 bytes. |
| CHECK(length >= AES_BLOCK_SIZE); |
| |
| // OpenSSL uses unsigned char. |
| unsigned char* data = reinterpret_cast<unsigned char*>(buffer); |
| |
| // Start with zero-filled initialization vector. |
| unsigned char iv[AES_BLOCK_SIZE]; |
| memset(iv, 0, AES_BLOCK_SIZE); |
| |
| // Size of partial last block handled via CTS. |
| int cts_byte_count = length % AES_BLOCK_SIZE; |
| |
| // If there no is no partial last block, then process using normal CBC. |
| if (cts_byte_count == 0) { |
| AES_cbc_encrypt(data, data, length, &key, iv, 0); |
| return OK; |
| } |
| |
| // Cipher text stealing (CTS) - Schneier Figure 9.5 p 196. |
| // In CTS mode, the last two blocks have been swapped. Block[n-1] is really |
| // the original block[n] combined with the low-order bytes of the original |
| // block[n-1], while block[n] is the high-order bytes of the original |
| // block[n-1] padded with zeros. |
| |
| // Block[0] - block[n-2] are handled with normal CBC. |
| int cbc_byte_count = length - cts_byte_count - AES_BLOCK_SIZE; |
| if (cbc_byte_count > 0) { |
| AES_cbc_encrypt(data, data, cbc_byte_count, &key, iv, 0); |
| // |data| points to block[n-1]. |
| data += cbc_byte_count; |
| } |
| |
| // Save block[n] to use as IV when decrypting block[n-1]. |
| unsigned char block_n[AES_BLOCK_SIZE]; |
| memset(block_n, 0, AES_BLOCK_SIZE); |
| memcpy(block_n, data + AES_BLOCK_SIZE, cts_byte_count); |
| |
| // Decrypt block[n-1] using block[n] as IV, consistent with the original |
| // block order. |
| AES_cbc_encrypt(data, data, AES_BLOCK_SIZE, &key, block_n, 0); |
| |
| // Return the stolen ciphertext: swap the high-order bytes of block[n] |
| // and block[n-1]. |
| for (int i = 0; i < cts_byte_count; i++) { |
| unsigned char temp = *(data + i); |
| *(data + i) = *(data + AES_BLOCK_SIZE + i); |
| *(data + AES_BLOCK_SIZE + i) = temp; |
| } |
| |
| // Decrypt block[n-1] using previous IV. |
| AES_cbc_encrypt(data, data, AES_BLOCK_SIZE, &key, iv, 0); |
| return OK; |
| } |
| |
| // PES header and ECM stream header layout |
| // |
| // processECM() receives the data_byte portion from the transport packet. |
| // Below is the layout of the first 16 bytes of the ECM PES packet. Here |
| // we don't parse them, we skip them and go to the ECM container directly. |
| // The layout is included here only for reference. |
| // |
| // 0-2: 0x00 00 01 = start code prefix. |
| // 3: 0xf0 = stream type (90 = ECM). |
| // 4-5: 0x00 00 = PES length (filled in later, this is the length of the |
| // PES header (16) plus the length of the ECM container). |
| // 6-7: 0x00 00 = ECM major version. |
| // 8-9: 0x00 01 = ECM minor version. |
| // 10-11: 0x00 00 = Crypto period ID (filled in later). |
| // 12-13: 0x00 00 = ECM container length (filled in later, either 84 or |
| // 166). |
| // 14-15: 0x00 00 = offset = 0. |
| |
| const static size_t kEcmHeaderLength = 16; |
| const static size_t kUserKeyLength = 16; |
| |
| status_t ClearKeyCasSession::updateECM( |
| KeyFetcher *keyFetcher, void *ecm, size_t size) { |
| if (keyFetcher == nullptr) { |
| return ERROR_CAS_NOT_PROVISIONED; |
| } |
| |
| if (size < kEcmHeaderLength) { |
| ALOGE("updateECM: invalid ecm size %zu", size); |
| return BAD_VALUE; |
| } |
| |
| Mutex::Autolock _lock(mKeyLock); |
| |
| if (mEcmBuffer != NULL && mEcmBuffer->capacity() == size |
| && !memcmp(mEcmBuffer->base(), ecm, size)) { |
| return OK; |
| } |
| |
| mEcmBuffer = ABuffer::CreateAsCopy(ecm, size); |
| mEcmBuffer->setRange(kEcmHeaderLength, size - kEcmHeaderLength); |
| |
| uint64_t asset_id; |
| std::vector<KeyFetcher::KeyInfo> keys; |
| status_t err = keyFetcher->ObtainKey(mEcmBuffer, &asset_id, &keys); |
| if (err != OK) { |
| ALOGE("updateECM: failed to obtain key (err=%d)", err); |
| return err; |
| } |
| |
| ALOGV("updateECM: %zu key(s) found", keys.size()); |
| for (size_t keyIndex = 0; keyIndex < keys.size(); keyIndex++) { |
| String8 str; |
| |
| const sp<ABuffer>& keyBytes = keys[keyIndex].key_bytes; |
| CHECK(keyBytes->size() == kUserKeyLength); |
| |
| int result = AES_set_decrypt_key( |
| reinterpret_cast<const uint8_t*>(keyBytes->data()), |
| AES_BLOCK_SIZE * 8, &mKeyInfo[keyIndex].contentKey); |
| mKeyInfo[keyIndex].valid = (result == 0); |
| if (!mKeyInfo[keyIndex].valid) { |
| ALOGE("updateECM: failed to set key %zu, key_id=%d", |
| keyIndex, keys[keyIndex].key_id); |
| } |
| } |
| return OK; |
| } |
| |
| // Decryption of a set of sub-samples |
| ssize_t ClearKeyCasSession::decrypt( |
| bool secure, DescramblerPlugin::ScramblingControl scramblingControl, |
| size_t numSubSamples, const DescramblerPlugin::SubSample *subSamples, |
| const void *srcPtr, void *dstPtr, AString * /* errorDetailMsg */) { |
| if (secure) { |
| return ERROR_CAS_CANNOT_HANDLE; |
| } |
| |
| scramblingControl = (DescramblerPlugin::ScramblingControl) |
| (scramblingControl & DescramblerPlugin::kScrambling_Mask_Key); |
| |
| AES_KEY contentKey; |
| |
| if (scramblingControl != DescramblerPlugin::kScrambling_Unscrambled) { |
| // Hold lock to get the key only to avoid contention for decryption |
| Mutex::Autolock _lock(mKeyLock); |
| |
| int32_t keyIndex = (scramblingControl & 1); |
| if (!mKeyInfo[keyIndex].valid) { |
| ALOGE("decrypt: key %d is invalid", keyIndex); |
| return ERROR_CAS_DECRYPT; |
| } |
| contentKey = mKeyInfo[keyIndex].contentKey; |
| } |
| |
| uint8_t *src = (uint8_t*)srcPtr; |
| uint8_t *dst = (uint8_t*)dstPtr; |
| |
| for (size_t i = 0; i < numSubSamples; i++) { |
| size_t numBytesinSubSample = subSamples[i].mNumBytesOfClearData |
| + subSamples[i].mNumBytesOfEncryptedData; |
| if (src != dst) { |
| memcpy(dst, src, numBytesinSubSample); |
| } |
| status_t err = OK; |
| // Don't decrypt if len < AES_BLOCK_SIZE. |
| // The last chunk shorter than AES_BLOCK_SIZE is not encrypted. |
| if (scramblingControl != DescramblerPlugin::kScrambling_Unscrambled |
| && subSamples[i].mNumBytesOfEncryptedData >= AES_BLOCK_SIZE) { |
| err = decryptPayload( |
| contentKey, |
| numBytesinSubSample, |
| subSamples[i].mNumBytesOfClearData, |
| (char *)dst); |
| } |
| |
| dst += numBytesinSubSample; |
| src += numBytesinSubSample; |
| } |
| return dst - (uint8_t *)dstPtr; |
| } |
| |
| // Decryption of a TS payload |
| status_t ClearKeyCasSession::decryptPayload( |
| const AES_KEY& key, size_t length, size_t offset, char* buffer) const { |
| CHECK(buffer); |
| |
| // Invariant: only call decryptPayload with TS packets with at least 16 |
| // bytes of payload (AES_BLOCK_SIZE). |
| |
| CHECK(length >= offset + AES_BLOCK_SIZE); |
| |
| return TpBlockCtsDecrypt(key, length - offset, buffer + offset); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////// |
| #undef LOG_TAG |
| #define LOG_TAG "ClearKeyDescramblerPlugin" |
| |
| bool ClearKeyDescramblerPlugin::requiresSecureDecoderComponent( |
| const char *mime) const { |
| ALOGV("requiresSecureDecoderComponent: mime=%s", mime); |
| return false; |
| } |
| |
| status_t ClearKeyDescramblerPlugin::setMediaCasSession( |
| const CasSessionId &sessionId) { |
| ALOGV("setMediaCasSession: sessionId=%s", sessionIdToString(sessionId).c_str()); |
| |
| std::shared_ptr<ClearKeyCasSession> session = |
| ClearKeySessionLibrary::get()->findSession(sessionId); |
| |
| if (session.get() == nullptr) { |
| ALOGE("ClearKeyDescramblerPlugin: session not found"); |
| return ERROR_CAS_SESSION_NOT_OPENED; |
| } |
| |
| std::atomic_store(&mCASSession, session); |
| return OK; |
| } |
| |
| ssize_t ClearKeyDescramblerPlugin::descramble( |
| bool secure, |
| ScramblingControl scramblingControl, |
| size_t numSubSamples, |
| const SubSample *subSamples, |
| const void *srcPtr, |
| int32_t srcOffset, |
| void *dstPtr, |
| int32_t dstOffset, |
| AString *errorDetailMsg) { |
| |
| ALOGV("descramble: secure=%d, sctrl=%d, subSamples=%s, " |
| "srcPtr=%p, dstPtr=%p, srcOffset=%d, dstOffset=%d", |
| (int)secure, (int)scramblingControl, |
| subSamplesToString(subSamples, numSubSamples).c_str(), |
| srcPtr, dstPtr, srcOffset, dstOffset); |
| |
| std::shared_ptr<ClearKeyCasSession> session = std::atomic_load(&mCASSession); |
| |
| if (session.get() == nullptr) { |
| ALOGE("Uninitialized CAS session!"); |
| return ERROR_CAS_DECRYPT_UNIT_NOT_INITIALIZED; |
| } |
| |
| return session->decrypt( |
| secure, scramblingControl, |
| numSubSamples, subSamples, |
| (uint8_t*)srcPtr + srcOffset, |
| dstPtr == NULL ? NULL : ((uint8_t*)dstPtr + dstOffset), |
| errorDetailMsg); |
| } |
| |
| // Conversion utilities |
| String8 ClearKeyDescramblerPlugin::arrayToString( |
| uint8_t const *array, size_t len) const |
| { |
| String8 result("{ "); |
| for (size_t i = 0; i < len; i++) { |
| result.appendFormat("0x%02x ", array[i]); |
| } |
| result += "}"; |
| return result; |
| } |
| |
| String8 ClearKeyDescramblerPlugin::subSamplesToString( |
| SubSample const *subSamples, size_t numSubSamples) const |
| { |
| String8 result; |
| for (size_t i = 0; i < numSubSamples; i++) { |
| result.appendFormat("[%zu] {clear:%u, encrypted:%u} ", i, |
| subSamples[i].mNumBytesOfClearData, |
| subSamples[i].mNumBytesOfEncryptedData); |
| } |
| return result; |
| } |
| |
| } // namespace clearkeycas |
| } // namespace android |