Initial import of Widevine Common Encryption DRM engine

Builds libwvmdrmengine.so, which is loaded by the new
MediaDrm APIs to support playback of Widevine/CENC
protected content.

Change-Id: I6f57dd37083dfd96c402cb9dd137c7d74edc8f1c
This commit is contained in:
Jeff Tinker
2013-03-21 17:39:02 -07:00
parent 38334efbe7
commit 1a8aa0dd05
211 changed files with 51913 additions and 144 deletions

View File

@@ -0,0 +1,68 @@
// Copyright 2012 Google Inc. All Rights Reserved.
#ifndef CDM_BASE_BUFFER_READER_H_
#define CDM_BASE_BUFFER_READER_H_
#include <stdint.h>
#include <string>
#include <vector>
#include "wv_cdm_types.h"
namespace wvcdm {
// Annotate a function indicating the caller must examine the return value.
// Use like:
// int foo() WARN_UNUSED_RESULT;
// To explicitly ignore a result, see |ignore_result()| in <base/basictypes.h>.
#if defined(COMPILER_GCC)
#define WARN_UNUSED_RESULT __attribute__((warn_unused_result))
#else
#define WARN_UNUSED_RESULT
#endif
class BufferReader {
public:
BufferReader(const uint8_t* buf, size_t size)
: buf_(buf), size_(size), pos_(0) {}
bool HasBytes(int count) { return (pos() + count <= size()); }
// Read a value from the stream, performing endian correction,
// and advance the stream pointer.
bool Read1(uint8_t* v) WARN_UNUSED_RESULT;
bool Read2(uint16_t* v) WARN_UNUSED_RESULT;
bool Read2s(int16_t* v) WARN_UNUSED_RESULT;
bool Read4(uint32_t* v) WARN_UNUSED_RESULT;
bool Read4s(int32_t* v) WARN_UNUSED_RESULT;
bool Read8(uint64_t* v) WARN_UNUSED_RESULT;
bool Read8s(int64_t* v) WARN_UNUSED_RESULT;
bool ReadString(std::string* str, int count) WARN_UNUSED_RESULT;
bool ReadVec(std::vector<uint8_t>* t, int count) WARN_UNUSED_RESULT;
// These variants read a 4-byte integer of the corresponding signedness and
// store it in the 8-byte return type.
bool Read4Into8(uint64_t* v) WARN_UNUSED_RESULT;
bool Read4sInto8s(int64_t* v) WARN_UNUSED_RESULT;
// Advance the stream by this many bytes.
bool SkipBytes(int nbytes) WARN_UNUSED_RESULT;
const uint8_t* data() const { return buf_; }
size_t size() const { return size_; }
size_t pos() const { return pos_; }
protected:
const uint8_t* buf_;
size_t size_;
size_t pos_;
template<typename T> bool Read(T* t) WARN_UNUSED_RESULT;
CORE_DISALLOW_COPY_AND_ASSIGN(BufferReader);
};
} // namespace wvcdm
#endif // CDM_BASE_BUFFER_READER_H_

View File

@@ -0,0 +1,123 @@
// Copyright 2013 Google Inc. All Rights Reserved.
#ifndef CDM_BASE_CDM_ENGINE_H_
#define CDM_BASE_CDM_ENGINE_H_
#include "timer.h"
#include "wv_cdm_types.h"
namespace wvcdm {
class CdmSession;
class WvCdmEventListener;
typedef std::map<CdmSessionId, CdmSession*> CdmSessionMap;
class CdmEngine : public TimerHandler {
public:
CdmEngine() {}
~CdmEngine();
// Session related methods
CdmResponseType OpenSession(const CdmKeySystem& key_system,
CdmSessionId* session_id);
CdmResponseType CloseSession(CdmSessionId& session_id);
// License related methods
// Construct a valid license request
CdmResponseType GenerateKeyRequest(const CdmSessionId& session_id,
bool is_key_system_present,
const CdmKeySystem& key_system,
const CdmInitData& init_data,
const CdmLicenseType license_type,
CdmNameValueMap& app_parameters,
CdmKeyMessage* key_request);
// Accept license response and extract key info.
CdmResponseType AddKey(const CdmSessionId& session_id,
bool is_key_system_init_data_present,
const CdmKeySystem& key_system,
const CdmInitData& init_data,
const CdmKeyResponse& key_data);
// Cancel session and unload keys.
CdmResponseType CancelKeyRequest(const CdmSessionId& session_id,
bool is_key_system_present,
const CdmKeySystem& key_system);
// Construct valid renewal request for the current session keys.
CdmResponseType GenerateRenewalRequest(const CdmSessionId& session_id,
bool is_key_system_init_data_present,
const CdmKeySystem& key_system,
const CdmInitData& init_data,
CdmKeyMessage* key_request);
// Accept renewal response and update key info.
CdmResponseType RenewKey(const CdmSessionId& session_id,
bool is_key_system_init_data_present,
const CdmKeySystem& key_system,
const CdmInitData& init_data,
const CdmKeyResponse& key_data);
// Query license information
CdmResponseType QueryKeyStatus(const CdmSessionId& session_id,
CdmNameValueMap* key_info);
// Provisioning related methods
CdmResponseType GetProvisioningRequest(CdmProvisioningRequest* request,
std::string* default_url);
CdmResponseType HandleProvisioningResponse(CdmProvisioningResponse& response);
// Secure stop related methods
CdmResponseType GetSecureStops(CdmSecureStops* secure_stops);
CdmResponseType ReleaseSecureStops(const CdmSecureStopReleaseMessage& message);
// Decryption and key related methods
// Accept encrypted buffer and return decrypted data.
CdmResponseType Decrypt(const CdmSessionId& session_id,
bool is_encrypted,
const KeyId& key_id,
const uint8_t* encrypted_buffer,
size_t encrypted_size,
const std::vector<uint8_t>& iv,
size_t block_offset,
void* decrypted_buffer);
// Is the key known to any session?
bool IsKeyValid(const KeyId& key_id);
// Event listener related methods
bool AttachEventListener(CdmSessionId& session_id,
WvCdmEventListener* listener);
bool DetachEventListener(CdmSessionId& session_id,
WvCdmEventListener* listener);
private:
// private methods
bool ValidateKeySystem(const CdmKeySystem& key_system);
// Cancel all sessions
bool CancelSessions();
// Parse a blob of multiple concatenated PSSH atoms to extract the first
// widevine pssh
// TODO(gmorgan): This should be done by the user of this class.
bool ExtractWidevinePssh(const CdmInitData& init_data,
CdmInitData* output);
// timer related methods to drive policy decisions
void EnablePolicyTimer();
void DisablePolicyTimer();
virtual void OnTimerEvent();
// instance variables
CdmSessionMap sessions_;
// policy timer
Timer policy_timer_;
CORE_DISALLOW_COPY_AND_ASSIGN(CdmEngine);
};
} // namespace wvcdm
#endif // CDM_BASE_CDM_ENGINE_H_

View File

@@ -0,0 +1,100 @@
// Copyright 2012 Google Inc. All Rights Reserved.
#ifndef CDM_BASE_CDM_SESSION_H_
#define CDM_BASE_CDM_SESSION_H_
#include <set>
#include "crypto_session.h"
#include "license.h"
#include "policy_engine.h"
#include "wv_cdm_event_listener.h"
#include "wv_cdm_types.h"
namespace wvcdm {
// TODO(kqyang): Do we need it? CdmKey not defined yet
// typedef std::map<KeyId, CdmKey*> CdmSessionKeys;
class CdmSession {
public:
CdmSession() : state_(INITIAL), session_id_(GenerateSessionId()) {}
~CdmSession() {}
bool Init();
bool DestroySession();
void set_key_system(const CdmKeySystem& ksystem) { key_system_ = ksystem; }
const CdmKeySystem& key_system() { return key_system_; }
const CdmSessionId& session_id() { return session_id_; }
bool VerifySession(const CdmKeySystem& key_system,
const CdmInitData& init_data);
CdmResponseType GenerateKeyRequest(const CdmInitData& init_data,
CdmKeyMessage* key_request);
// AddKey() - Accept license response and extract key info.
CdmResponseType AddKey(const CdmKeyResponse& key_response);
// CancelKeyRequest() - Cancel session.
CdmResponseType CancelKeyRequest();
// Decrypt() - Accept encrypted buffer and return decrypted data.
CdmResponseType Decrypt(const uint8_t* encrypted_buffer,
size_t encrypted_size,
size_t block_offset,
const std::string& iv,
const KeyId& key_id,
uint8_t* decrypted_buffer);
// License renewal
// GenerateRenewalRequest() - Construct valid renewal request for the current
// session keys.
CdmResponseType GenerateRenewalRequest(CdmKeyMessage* key_request);
// RenewKey() - Accept renewal response and update key info.
CdmResponseType RenewKey(const CdmKeyResponse& key_response);
bool IsKeyValid(const KeyId& key_id);
bool AttachEventListener(WvCdmEventListener* listener);
bool DetachEventListener(WvCdmEventListener* listener);
void OnTimerEvent();
private:
// Generate unique ID for each new session.
CdmSessionId GenerateSessionId();
typedef enum {
INITIAL,
LICENSE_REQUESTED,
LICENSE_RESPONSE_DONE,
RENEWAL_ENABLED,
RENEWAL_REQUESTED,
LICENSE_EXPIRED
} CdmSessionState;
// instance variables
CdmSessionState state_;
const CdmSessionId session_id_;
CdmKeySystem key_system_;
CdmLicense license_parser_;
CryptoSession* crypto_session_;
PolicyEngine policy_engine_;
std::set<WvCdmEventListener*> listeners_;
// TODO(kqyang): CdmKey not defined yet
// CdmSessionKeys session_keys_;
CORE_DISALLOW_COPY_AND_ASSIGN(CdmSession);
};
} // namespace wvcdm
#endif // CDM_BASE_CDM_SESSION_H_

View File

@@ -0,0 +1,19 @@
// Copyright 2013 Google Inc. All Rights Reserved.
//
// Clock - Platform independent interface for a time library
//
#ifndef CDM_BASE_CLOCK_H_
#define CDM_BASE_CLOCK_H_
#include <stdint.h>
namespace wvcdm {
// Provides time related information. The implementation is platform dependent.
// Provides the number of seconds since an epoch (00:00 hours, Jan 1, 1970 UTC)
int64_t GetCurrentTime();
}; // namespace wvcdm
#endif // CDM_BASE_CLOCK_H_

View File

@@ -0,0 +1,61 @@
// Copyright 2012 Google Inc. All Rights Reserved.
//
// OEMCrypto Client - wrapper class for C-style OEMCrypto interface
//
#ifndef CDM_BASE_CRYPTO_ENGINE_H_
#define CDM_BASE_CRYPTO_ENGINE_H_
#include "crypto_session.h"
#include "lock.h"
#include "wv_cdm_types.h"
namespace wvcdm {
typedef std::map<CdmSessionId,CryptoSession*> CryptoSessionMap;
class CryptoEngine {
friend class CryptoSession;
private:
CryptoEngine();
~CryptoEngine();
public:
// get an instance of Crypto engine
static CryptoEngine* GetInstance();
bool Init();
bool Terminate();
bool ValidateKeybox();
CryptoSession* CreateSession(const CdmSessionId& session_id);
CryptoSession* FindSession(const CdmSessionId& session_id);
bool DestroySession(const CdmSessionId& session_id);
bool DestroySessions();
bool GetToken(std::string* token);
private:
void DeleteInstance();
static CryptoEngine* CreateSingleton();
CryptoSession* FindSessionInternal(const CdmSessionId& session_id);
static CryptoEngine* crypto_engine_;
static Lock crypto_engine_lock_;
bool initialized_;
mutable Lock crypto_lock_;
mutable Lock sessions_lock_;
CryptoSessionMap sessions_;
CORE_DISALLOW_COPY_AND_ASSIGN(CryptoEngine);
};
}; // namespace wvcdm
#endif // CDM_BASE_CRYPTO_ENGINE_H_

View File

@@ -0,0 +1,44 @@
// Copyright 2012 Google Inc. All Rights Reserved.
//
// OEMCrypto Client - wrapper class for C-style OEMCrypto interface
//
#ifndef CDM_BASE_CRYPTO_KEY_H_
#define CDM_BASE_CRYPTO_KEY_H_
#include "wv_cdm_types.h"
namespace wvcdm {
class CryptoKey {
public:
CryptoKey() {};
~CryptoKey() {};
const std::string& key_id() const { return key_id_; }
const std::string& key_data() const { return key_data_; }
const std::string& key_data_iv() const { return key_data_iv_; }
const std::string& key_control() const { return key_control_; }
const std::string& key_control_iv() const { return key_control_iv_; }
void set_key_id(const std::string& key_id) { key_id_ = key_id; }
void set_key_data(const std::string& key_data) { key_data_ = key_data; }
void set_key_data_iv(const std::string& iv) { key_data_iv_ = iv; }
void set_key_control(const std::string& ctl) { key_control_ = ctl; }
void set_key_control_iv(const std::string& ctl_iv) {
key_control_iv_ = ctl_iv;
}
bool HasKeyControl() const { return key_control_.size() >= 16; }
private:
std::string key_id_;
std::string key_data_iv_;
std::string key_data_;
std::string key_control_;
std::string key_control_iv_;
CORE_DISALLOW_COPY_AND_ASSIGN(CryptoKey);
};
}; // namespace wvcdm
#endif // CDM_BASE_CRYPTO_KEY_H_

View File

@@ -0,0 +1,81 @@
// Copyright 2012 Google Inc. All Rights Reserved.
//
// OEMCrypto Client - wrapper class for C-style OEMCrypto interface
//
#ifndef CDM_BASE_CRYPTO_SESSSION_H_
#define CDM_BASE_CRYPTO_SESSSION_H_
#include <string>
#include <map>
#include "crypto_key.h"
#include "wv_cdm_types.h"
namespace wvcdm {
typedef std::map<CryptoKeyId,CryptoKey*> CryptoKeyMap;
// TODO(gmorgan): fill out input and output descriptors
typedef void* InputDescriptor;
typedef void* OutputDescriptor;
class CryptoSession {
public:
CryptoSession();
explicit CryptoSession(const std::string& sname);
~CryptoSession();
bool Open();
void Close();
bool IsValid() { return valid_; }
bool IsOpen() { return open_; }
bool SuccessStatus();
CryptoResult session_status() { return session_status_; }
CryptoSessionId oec_session_id() { return oec_session_id_; }
CdmSessionId cdm_session_id() { return cdm_session_id_; }
// Key request/response
void GenerateRequestId(std::string& req_id_str);
bool PrepareRequest(const std::string& key_deriv_message,
std::string* signature);
bool PrepareRenewalRequest(const std::string& message,
std::string* signature);
bool LoadKeys(const std::string& message,
const std::string& signature,
const std::string& mac_key_iv,
const std::string& mac_key,
int num_keys,
const CryptoKey* key_array);
bool RefreshKeys(const std::string& message,
const std::string& signature,
int num_keys,
const CryptoKey* key_array);
bool GenerateNonce(uint32_t* nonce);
// Media data path
bool SelectKey(const std::string& key_id);
bool Decrypt(const InputDescriptor input, OutputDescriptor output);
private:
void GenerateMacContext(const std::string& input_context,
std::string* deriv_context);
void GenerateEncryptContext(const std::string& input_context,
std::string* deriv_context);
size_t GetOffset(std::string message, std::string field);
bool valid_;
bool open_;
CdmSessionId cdm_session_id_;
CryptoSessionId oec_session_id_;
CryptoResult session_status_;
CryptoKeyMap keys_;
CORE_DISALLOW_COPY_AND_ASSIGN(CryptoSession);
};
}; // namespace wvcdm
#endif // CDM_BASE_CRYPTO_SESSSION_H_

View File

@@ -0,0 +1,41 @@
// Copyright 2012 Google Inc. All Rights Reserved.
#ifndef CDM_BASE_LICENSE_H_
#define CDM_BASE_LICENSE_H_
#include "license_protocol.pb.h"
#include "wv_cdm_types.h"
namespace wvcdm {
using video_widevine_server::sdk::LicenseIdentification;
class CryptoSession;
class CdmLicense {
public:
CdmLicense();
~CdmLicense();
bool Init(const std::string& token, CryptoSession* session);
bool PrepareKeyRequest(const CdmInitData& init_data,
CdmKeyMessage* signed_request);
bool PrepareKeyRenewalRequest(CdmKeyMessage* signed_request);
bool HandleKeyResponse(const CdmKeyResponse& license_response);
bool HandleKeyRenewalResponse(const CdmKeyResponse& license_response);
private:
LicenseIdentification license_id_;
CryptoSession* session_;
std::string token_;
CORE_DISALLOW_COPY_AND_ASSIGN(CdmLicense);
};
} // namespace wvcdm
#endif // CDM_BASE_LICENSE_H_

View File

@@ -0,0 +1,54 @@
// Copyright 2013 Google Inc. All Rights Reserved.
//
// Lock - Platform independent interface for a Mutex class
//
#ifndef CDM_BASE_LOCK_H_
#define CDM_BASE_LOCK_H_
#include "wv_cdm_types.h"
namespace wvcdm {
// Simple lock class. The implementation is platform dependent.
//
// The lock must be unlocked by the thread that locked it.
// The lock is also not recursive (ie. cannot be taken multiple times).
class Lock {
public:
Lock();
~Lock();
void Acquire();
void Release();
// Acquires a lock if not held and returns true.
// Returns false if the lock is held by another thread.
bool Try();
friend class AutoLock;
private:
class Impl;
Impl *impl_;
CORE_DISALLOW_COPY_AND_ASSIGN(Lock);
};
// Manages the lock automatically. It will be locked when AutoLock
// is constructed and release when AutoLock goes out of scope
class AutoLock {
public:
explicit AutoLock(Lock& lock);
explicit AutoLock(Lock* lock);
~AutoLock();
private:
class Impl;
Impl *impl_;
CORE_DISALLOW_COPY_AND_ASSIGN(AutoLock);
};
}; // namespace wvcdm
#endif // CDM_BASE_LOCK_H_

View File

@@ -0,0 +1,31 @@
// Copyright 2013 Google Inc. All Rights Reserved.
//
// Log - Platform independent interface for a Logging class
//
#ifndef CDM_BASE_LOG_H_
#define CDM_BASE_LOG_H_
namespace wvcdm {
// Simple logging class. The implementation is platform dependent.
typedef enum {
LOG_ERROR,
LOG_WARN,
LOG_INFO,
LOG_DEBUG,
LOG_VERBOSE
} LogPriority;
void log_write(LogPriority priority, const char *fmt, ...);
// Log APIs
#define LOGE(...) ((void)log_write(wvcdm::LOG_ERROR, __VA_ARGS__))
#define LOGW(...) ((void)log_write(wvcdm::LOG_WARN, __VA_ARGS__))
#define LOGI(...) ((void)log_write(wvcdm::LOG_INFO, __VA_ARGS__))
#define LOGD(...) ((void)log_write(wvcdm::LOG_DEBUG, __VA_ARGS__))
#define LOGV(...) ((void)log_write(wvcdm::LOG_VERBOSE, __VA_ARGS__))
}; // namespace wvcdm
#endif // CDM_BASE_LOG_H_

View File

@@ -0,0 +1,83 @@
// Copyright 2013 Google Inc. All Rights Reserved.
#ifndef CDM_BASE_POLICY_ENGINE_H_
#define CDM_BASE_POLICY_ENGINE_H_
#include <string>
#include "license_protocol.pb.h"
#include "wv_cdm_types.h"
namespace wvcdm {
// This acts as an oracle that basically says "Yes(true) you may still decrypt
// or no(false) you may not decrypt this data anymore."
class PolicyEngine {
public:
PolicyEngine();
~PolicyEngine();
// |current_time| is used to check if license has to be renewed or expired.
void OnTimerEvent(int64_t current_time, bool event_occurred, CdmEventType& event);
// SetLicense is used in handling the initial license response. It stores
// an exact copy of the policy information stored in the license.
// The license state transitions to kLicenseStateCanPlay if the license
// permits playback.
void SetLicense(const video_widevine_server::sdk::License& license);
// UpdateLicense is used in handling a license response for a renewal request.
// The response may only contain any policy fields that have changed. In this
// case an exact copy is not what we want to happen. We also will receive an
// updated license_start_time from the server. The license will transition to
// kLicenseStateCanPlay if the license permits playback.
void UpdateLicense(const video_widevine_server::sdk::License& license);
const video_widevine_server::sdk::LicenseIdentification& license_id() {
return license_id_;
}
private:
typedef enum {
kLicenseStateInitial,
kLicenseStateCanPlay,
kLicenseStateCannotPlay,
kLicenseStateNeedRenewal,
kLicenseStateWaitingLicenseUpdate,
kLicenseStateExpired
} LicenseState;
bool IsLicenseDurationExpired(int64_t current_time);
bool IsRenewalDelayExpired(int64_t current_time);
bool IsRenewalRecoveryDurationExpired(int64_t current_time);
bool IsRenewalRetryIntervalExpired(int64_t current_time);
void UpdateRenewalRequest(int64_t current_time);
LicenseState license_state_;
// This is the current policy information for this license. This gets updated
// as license renewals occur.
video_widevine_server::sdk::License::Policy policy_;
// This is the license id field from server response. This data gets passed
// back to the server in each renewal request. When we get a renewal response
// from the license server we will get an updated id field.
video_widevine_server::sdk::LicenseIdentification license_id_;
// This is the license start time that gets sent from the server in each
// license request or renewal.
int64_t license_start_time_;
// This is used as a reference point for policy management. This value
// represents an offset from license_start_time_. This is used to calculate
// the time where renewal retries should occur.
int64_t next_renewal_time_;
int64_t policy_max_duration_seconds_;
CORE_DISALLOW_COPY_AND_ASSIGN(PolicyEngine);
};
} // wvcdm
#endif // CDM_BASE_POLICY_ENGINE_H_

View File

@@ -0,0 +1,25 @@
// Copyright 2013 Google Inc. All Rights Reserved.
#ifndef CDM_BASE_STRING_CONVERSIONS_H_
#define CDM_BASE_STRING_CONVERSIONS_H_
#include <stddef.h>
#include <stdint.h>
#include <string>
#include <vector>
namespace wvcdm {
std::vector<uint8_t> a2b_hex(const std::string& b);
std::string a2bs_hex(const std::string& b);
std::string b2a_hex(const std::vector<uint8_t>& b);
std::string b2a_hex(const std::string& b);
std::string Base64SafeEncode(const std::vector<uint8_t>& bin_input);
std::vector<uint8_t> Base64SafeDecode(const std::string& bin_input);
std::string HexEncode(const uint8_t* bytes, unsigned size);
std::string IntToString(int value);
std::string UintToString(unsigned int value);
}; // namespace wvcdm
#endif // CDM_BASE_STRING_CONVERSIONS_H_

View File

@@ -0,0 +1,50 @@
// Copyright 2013 Google Inc. All Rights Reserved.
//
// Timer - Platform independent interface for a Timer class
//
#ifndef CDM_BASE_TIMER_H_
#define CDM_BASE_TIMER_H_
#include "wv_cdm_types.h"
namespace wvcdm {
// Timer Handler class.
//
// Derive from this class if you wish to receive events when the timer
// expires. Provide the handler when setting up a new Timer.
class TimerHandler {
public:
TimerHandler() {};
virtual ~TimerHandler() {};
virtual void OnTimerEvent() = 0;
};
// Timer class. The implementation is platform dependent.
//
// This class provides a simple recurring timer API. The class receiving
// timer expiry events should derive from TimerHandler.
// Specify the receiver class and the periodicty of timer events when
// the timer is initiated by calling Start.
class Timer {
public:
Timer();
~Timer();
void Start(TimerHandler *handler, uint32_t time_in_secs);
void Stop();
bool IsRunning();
private:
class Impl;
Impl *impl_;
CORE_DISALLOW_COPY_AND_ASSIGN(Timer);
};
}; // namespace wvcdm
#endif // CDM_BASE_TIMER_H_

View File

@@ -0,0 +1,18 @@
// Copyright 2012 Google Inc. All Rights Reserved.
#ifndef CDM_BASE_WV_CDM_CONSTANTS_H_
#define CDM_BASE_WV_CDM_CONSTANTS_H_
namespace wvcdm {
static const size_t KEY_CONTROL_SIZE = 16;
// TODO(kqyang): Key ID size is not fixed in spec, but conventionally we
// always use 16 bytes key id. We'll need to update oemcrypto to support
// variable size key id.
static const size_t KEY_ID_SIZE = 16;
static const size_t KEY_IV_SIZE = 16;
static const size_t KEY_PAD_SIZE = 16;
static const size_t KEY_SIZE = 16;
static const size_t MAC_KEY_SIZE = 32;
} // namespace wvcdm
#endif // CDM_BASE_WV_CDM_CONSTANTS_H_

View File

@@ -0,0 +1,28 @@
// Copyright 2012 Google Inc. All Rights Reserved.
#ifndef CDM_BASE_WV_CDM_EVENT_LISTENER_H_
#define CDM_BASE_WV_CDM_EVENT_LISTENER_H_
#include "wv_cdm_types.h"
namespace wvcdm {
// Listener for events from the Content Decryption Module.
// The caller of the CDM API must provide an implementation for onEvent
// and signal its intent by using the Attach/DetachEventListener methods
// in the WvContentDecryptionModule class.
class WvCdmEventListener {
public:
WvCdmEventListener() {}
virtual ~WvCdmEventListener() {}
virtual void onEvent(const CdmSessionId& session_id,
CdmEventType cdm_event) = 0;
private:
CORE_DISALLOW_COPY_AND_ASSIGN(WvCdmEventListener);
};
} // namespace wvcdm
#endif // CDM_BASE_WV_CDM_EVENT_LISTENER_H_

View File

@@ -0,0 +1,60 @@
// Copyright 2013 Google Inc. All Rights Reserved.
#ifndef CDM_BASE_WV_CDM_TYPES_H_
#define CDM_BASE_WV_CDM_TYPES_H_
#include <map>
#include <stdint.h>
#include <string>
#include <vector>
namespace wvcdm {
typedef std::string CdmKeySystem;
typedef std::string CdmInitData;
typedef std::string CdmKeyMessage;
typedef std::string CdmKeyResponse;
typedef std::string KeyId;
typedef std::string CdmSessionId;
typedef std::string RequestId;
typedef uint32_t CryptoResult;
typedef uint32_t CryptoSessionId;
typedef std::string CryptoKeyId;
typedef std::map<std::string, std::string> CdmNameValueMap;
typedef std::vector<std::string> CdmSecureStops;
typedef std::vector<uint8_t> CdmSecureStopReleaseMessage;
typedef std::string CdmProvisioningRequest;
typedef std::string CdmProvisioningResponse;
enum CdmResponseType {
NO_ERROR,
UNKNOWN_ERROR,
KEY_ADDED,
KEY_ERROR,
KEY_MESSAGE,
NEED_KEY,
KEY_CANCELED,
};
#define CORE_DISALLOW_COPY_AND_ASSIGN(TypeName) \
TypeName(const TypeName&); \
void operator=(const TypeName&)
enum CdmEventType {
LICENSE_EXPIRED_EVENT,
LICENSE_RENEWAL_NEEDED_EVENT
};
enum CdmLicenseType {
kLicenseTypeOffline,
kLicenseTypeStreaming
};
// forward class references
class KeyMessage;
class Request;
class Key;
} // namespace wvcdm
#endif // CDM_BASE_WV_CDM_TYPES_H_

View File

@@ -0,0 +1,94 @@
// Copyright 2012 Google Inc. All Rights Reserved.
#include "buffer_reader.h"
#include "log.h"
namespace wvcdm {
bool BufferReader::Read1(uint8_t* v) {
if (!HasBytes(1)) {
LOGE("BufferReader::Read1 : Failure while parsing: Not enough bytes (1)");
return false;
}
*v = buf_[pos_++];
return true;
}
// Internal implementation of multi-byte reads
template<typename T> bool BufferReader::Read(T* v) {
if (!HasBytes(sizeof(T))) {
LOGE("BufferReader::Read<T> : Failure while parsing: Not enough bytes (%u)", sizeof(T));
return false;
}
T tmp = 0;
for (size_t i = 0; i < sizeof(T); i++) {
tmp <<= 8;
tmp += buf_[pos_++];
}
*v = tmp;
return true;
}
bool BufferReader::Read2(uint16_t* v) { return Read(v); }
bool BufferReader::Read2s(int16_t* v) { return Read(v); }
bool BufferReader::Read4(uint32_t* v) { return Read(v); }
bool BufferReader::Read4s(int32_t* v) { return Read(v); }
bool BufferReader::Read8(uint64_t* v) { return Read(v); }
bool BufferReader::Read8s(int64_t* v) { return Read(v); }
bool BufferReader::ReadString(std::string* str, int count) {
if (!HasBytes(count)) {
LOGE("BufferReader::ReadString : Failure while parsing: Not enough bytes (%d)", count);
return false;
}
str->assign(buf_ + pos_, buf_ + pos_ + count);
pos_ += count;
return true;
}
bool BufferReader::ReadVec(std::vector<uint8_t>* vec, int count) {
if (!HasBytes(count)) {
LOGE("BufferReader::ReadVec : Failure while parsing: Not enough bytes (%d)", count);
return false;
}
vec->clear();
vec->insert(vec->end(), buf_ + pos_, buf_ + pos_ + count);
pos_ += count;
return true;
}
bool BufferReader::SkipBytes(int bytes) {
if (!HasBytes(bytes)) {
LOGE("BufferReader::SkipBytes : Failure while parsing: Not enough bytes (%d)", bytes);
return false;
}
pos_ += bytes;
return true;
}
bool BufferReader::Read4Into8(uint64_t* v) {
uint32_t tmp;
if (!Read4(&tmp)) {
return false;
}
*v = tmp;
return true;
}
bool BufferReader::Read4sInto8s(int64_t* v) {
// Beware of the need for sign extension.
int32_t tmp;
if (!Read4s(&tmp)) {
return false;
}
*v = tmp;
return true;
}
} // namespace wvcdm

View File

@@ -0,0 +1,463 @@
// Copyright 2013 Google Inc. All Rights Reserved.
#include "cdm_engine.h"
#include <iostream>
#include "buffer_reader.h"
#include "cdm_session.h"
#include "log.h"
#include "wv_cdm_constants.h"
#include "wv_cdm_event_listener.h"
#ifndef CDM_POLICY_TIMER_DURATION_SECONDS
#define CDM_POLICY_TIMER_DURATION_SECONDS 1
#endif
namespace wvcdm {
typedef std::map<CdmSessionId,CdmSession*>::iterator CdmSessionIter;
CdmEngine::~CdmEngine() {
CancelSessions();
CdmSessionMap::iterator i(sessions_.begin());
for (; i != sessions_.end(); ++i)
delete i->second;
sessions_.clear();
}
CdmResponseType CdmEngine::OpenSession(
const CdmKeySystem& key_system,
CdmSessionId* session_id) {
LOGI("CdmEngine::OpenSession");
if (!ValidateKeySystem(key_system)) {
LOGI("CdmEngine::OpenSession: invalid key_system = %s", key_system.c_str());
return KEY_ERROR;
}
if (!session_id) {
LOGE("CdmEngine::OpenSession: no session ID destination provided");
return KEY_ERROR;
}
// TODO(edwinwong, rfrias): Save key_system in session for validation checks
CdmSession* new_session = new CdmSession();
if (!new_session) {
return KEY_ERROR;
}
if (new_session->session_id().empty()) {
LOGE("CdmEngine::OpenSession: failure to generate session ID");
delete(new_session);
return UNKNOWN_ERROR;
}
CdmSessionId new_session_id = new_session->session_id();
if (!new_session->Init()) {
LOGE("CdmEngine::OpenSession: bad session init");
delete(new_session);
return UNKNOWN_ERROR;
}
sessions_[new_session_id] = new_session;
*session_id = new_session_id;
return NO_ERROR;
}
CdmResponseType CdmEngine::CloseSession(CdmSessionId& session_id) {
LOGI("CdmEngine::CloseSession");
CdmSession* cdm_session = sessions_[session_id];
if (!cdm_session) {
LOGE("CdmEngine::CloseSession: session not found = %s", session_id.c_str());
return KEY_ERROR;
}
sessions_.erase(session_id);
cdm_session->DestroySession();
delete cdm_session;
return NO_ERROR;
}
CdmResponseType CdmEngine::GenerateKeyRequest(
const CdmSessionId& session_id,
bool is_key_system_present,
const CdmKeySystem& key_system,
const CdmInitData& init_data,
const CdmLicenseType license_type,
CdmNameValueMap& app_parameters,
CdmKeyMessage* key_request) {
LOGI("CdmEngine::GenerateKeyRequest");
CdmSession* session = sessions_[session_id];
if (!session) {
LOGE("CdmEngine::GenerateKeyRequest: session_id not found = %s", session_id.c_str());
return KEY_ERROR;
}
if (is_key_system_present) {
// TODO(edwinwong, rfrias): validate key_system has not changed
}
if (init_data.empty()) {
LOGE("CdmEngine::GenerateKeyRequest: no init_data provided");
return KEY_ERROR;
}
if (!key_request) {
LOGE("CdmEngine::GenerateKeyRequest: no key request destination provided");
return KEY_ERROR;
}
CdmInitData extracted_pssh;
if (!ExtractWidevinePssh(init_data, &extracted_pssh)) {
key_request->clear();
return KEY_ERROR;
}
key_request->clear();
// TODO(edwinwong, rfrias): need to pass in license type and app parameters
CdmResponseType sts = session->GenerateKeyRequest(extracted_pssh,
key_request);
if (KEY_MESSAGE != sts) {
LOGE("CdmEngine::GenerateKeyRequest: key request generation failed, sts=%d",
(int)sts);
return sts;
}
// TODO(edwinwong, rfrias): persist init_data, license_type, app_parameters
// in session
return KEY_MESSAGE;
}
CdmResponseType CdmEngine::AddKey(
const CdmSessionId& session_id,
bool is_key_system_init_data_present,
const CdmKeySystem& key_system,
const CdmInitData& init_data,
const CdmKeyResponse& key_data) {
LOGI("CdmEngine::AddKey");
CdmSession* session = sessions_[session_id];
if (!session) {
LOGE("CdmEngine::AddKey: session_id not found = %s", session_id.c_str());
return KEY_ERROR;
}
if (is_key_system_init_data_present) {
// TODO(edwinwong, rfrias): validate key_system has not changed
}
if (is_key_system_init_data_present) {
// TODO(edwinwong, rfrias): validate init_data has not changed
}
if (key_data.empty()) {
LOGE("CdmEngine::AddKey: no key_data");
return KEY_ERROR;
}
CdmResponseType sts = session->AddKey(key_data);
if (KEY_ADDED != sts) {
LOGE("CdmEngine::AddKey: keys not added, result = %d", (int)sts);
}
return sts;
}
CdmResponseType CdmEngine::CancelKeyRequest(
const CdmSessionId& session_id,
bool is_key_system_present,
const CdmKeySystem& key_system) {
LOGI("CdmEngine::CancelKeyRequest");
//TODO(gmorgan): Issue: what is semantics of canceling a key request. Should
//this call cancel all keys for the session?
// TODO(jfore): We should disable the policy timer here if there are no
// active sessions. Sessions are currently not being destroyed here. We can
// add this logic once the semantics of canceling the key is worked out.
CdmSession* session = sessions_[session_id];
if (!session) {
LOGE("CdmEngine::CancelKeyRequest: session_id not found = %s", session_id.c_str());
return KEY_ERROR;
}
if (is_key_system_present) {
// TODO(edwinwong, rfrias): validate key_system has not changed
}
// TODO(edwinwong, rfrias): unload keys here
return NO_ERROR;
}
CdmResponseType CdmEngine::GenerateRenewalRequest(
const CdmSessionId& session_id,
bool is_key_system_init_data_present,
const CdmKeySystem& key_system,
const CdmInitData& init_data,
CdmKeyMessage* key_request) {
LOGI("CdmEngine::GenerateRenewalRequest");
CdmSession* session = sessions_[session_id];
if (!session) {
LOGE("CdmEngine::GenerateRenewalRequest: session_id not found = %s", session_id.c_str());
return KEY_ERROR;
}
if (is_key_system_init_data_present) {
// TODO(edwinwong, rfrias): validate key_system has not changed
}
if (is_key_system_init_data_present) {
// TODO(edwinwong, rfrias): validate init_data has not changed
}
if (!key_request) {
LOGE("CdmEngine::GenerateRenewalRequest: no key request destination provided");
return KEY_ERROR;
}
key_request->clear();
CdmResponseType sts = session->GenerateRenewalRequest(key_request);
if (KEY_MESSAGE != sts) {
LOGE("CdmEngine::GenerateRenewalRequest: key request generation failed, sts=%d",
(int)sts);
return sts;
}
return KEY_MESSAGE;
}
CdmResponseType CdmEngine::RenewKey(
const CdmSessionId& session_id,
bool is_key_system_init_data_present,
const CdmKeySystem& key_system,
const CdmInitData& init_data,
const CdmKeyResponse& key_data) {
LOGI("CdmEngine::RenewKey");
CdmSession* session = sessions_[session_id];
if (!session) {
LOGE("CdmEngine::RenewKey: session_id not found = %s", session_id.c_str());
return KEY_ERROR;
}
if (is_key_system_init_data_present) {
// TODO(edwinwong, rfrias): validate key_system has not changed
}
if (is_key_system_init_data_present) {
// TODO(edwinwong, rfrias): validate init_data has not changed
}
if (key_data.empty()) {
LOGE("CdmEngine::RenewKey: no key_data");
return KEY_ERROR;
}
CdmResponseType sts = session->RenewKey(key_data);
if (KEY_ADDED != sts) {
LOGE("CdmEngine::RenewKey: keys not added, sts=%d", (int)sts);
return sts;
}
return KEY_ADDED;
}
CdmResponseType CdmEngine::QueryKeyStatus(
const CdmSessionId& session_id,
CdmNameValueMap* key_info) {
// TODO(edwinwong, rfrias): add implementation
return NO_ERROR;
}
CdmResponseType CdmEngine::GetProvisioningRequest(
CdmProvisioningRequest* request,
std::string* default_url) {
// TODO(edwinwong, rfrias): add implementation
return NO_ERROR;
}
CdmResponseType CdmEngine::HandleProvisioningResponse(
CdmProvisioningResponse& response) {
// TODO(edwinwong, rfrias): add implementation
return NO_ERROR;
}
CdmResponseType CdmEngine::GetSecureStops(
CdmSecureStops* secure_stops) {
// TODO(edwinwong, rfrias): add implementation
return NO_ERROR;
}
CdmResponseType CdmEngine::ReleaseSecureStops(
const CdmSecureStopReleaseMessage& message) {
// TODO(edwinwong, rfrias): add implementation
return NO_ERROR;
}
CdmResponseType CdmEngine::Decrypt(
const CdmSessionId& session_id,
bool is_encrypted,
const KeyId& key_id,
const uint8_t* encrypted_buffer,
size_t encrypted_size,
const std::vector<uint8_t>& iv,
size_t block_offset,
void* decrypted_buffer) {
CdmSession* session = sessions_[session_id];
if (!session) {
LOGW("CdmEngine::Decrypt: session_id not found = %s", session_id.c_str());
return KEY_ERROR;
}
// TODO(edwinwong, rfrias): Need to add implemenation and hook up
// decryption though to oem_crypto
return NO_ERROR;
}
bool CdmEngine::IsKeyValid(const KeyId& key_id) {
for (CdmSessionIter iter = sessions_.begin();
iter != sessions_.end(); ++iter) {
if (iter->second->IsKeyValid(key_id)) {
return true;
}
}
return false;
}
bool CdmEngine::AttachEventListener(
CdmSessionId& session_id,
WvCdmEventListener* listener) {
CdmSessionIter iter = sessions_.find(session_id);
if (iter == sessions_.end()) {
return false;
}
return iter->second->AttachEventListener(listener);
}
bool CdmEngine::DetachEventListener(
CdmSessionId& session_id,
WvCdmEventListener* listener) {
CdmSessionIter iter = sessions_.find(session_id);
if (iter == sessions_.end()) {
return false;
}
return iter->second->DetachEventListener(listener);
}
bool CdmEngine::ValidateKeySystem(const CdmKeySystem& key_system) {
return (key_system.find("widevine") != std::string::npos);
}
bool CdmEngine::CancelSessions() {
// TODO(gmorgan) Implement CancelSessions()
return true;
}
// Parse a blob of multiple concatenated PSSH atoms to extract the first
// widevine pssh
// TODO(kqyang): temporary workaround - remove after b/7928472 is resolved
bool CdmEngine::ExtractWidevinePssh(
const CdmInitData& init_data, CdmInitData* output) {
BufferReader reader(
reinterpret_cast<const uint8_t*>(init_data.data()), init_data.length());
// TODO(kqyang): Extracted from an actual init_data;
// Need to find out where it comes from.
static const uint8_t kWidevineSystemId[] = {
0xED, 0xEF, 0x8B, 0xA9, 0x79, 0xD6, 0x4A, 0xCE,
0xA3, 0xC8, 0x27, 0xDC, 0xD5, 0x1D, 0x21, 0xED,
};
// one PSSH blob consists of:
// 4 byte size of the PSSH atom, inclusive
// "pssh"
// 4 byte flags, value 0
// 16 byte system id
// 4 byte size of PSSH data, exclusive
while (1) {
// size of PSSH atom, used for skipping
uint32_t size;
if (!reader.Read4(&size)) return false;
// "pssh"
std::vector<uint8_t> pssh;
if (!reader.ReadVec(&pssh, 4)) return false;
if (memcmp(&pssh[0], "pssh", 4)) return false;
// flags
uint32_t flags;
if (!reader.Read4(&flags)) return false;
if (flags != 0) return false;
// system id
std::vector<uint8_t> system_id;
if (!reader.ReadVec(&system_id, sizeof(kWidevineSystemId))) return false;
if (memcmp(&system_id[0], kWidevineSystemId,
sizeof(kWidevineSystemId))) {
// skip the remaining contents of the atom,
// after size field, atom name, flags and system id
if (!reader.SkipBytes(
size - 4 - 4 - 4 - sizeof(kWidevineSystemId))) return false;
continue;
}
// size of PSSH box
uint32_t pssh_length;
if (!reader.Read4(&pssh_length)) return false;
output->clear();
if (!reader.ReadString(output, pssh_length)) return false;
return true;
}
// we did not find a matching record
return false;
}
void CdmEngine::EnablePolicyTimer() {
if (!policy_timer_.IsRunning())
policy_timer_.Start(this, CDM_POLICY_TIMER_DURATION_SECONDS);
}
void CdmEngine::DisablePolicyTimer() {
if (policy_timer_.IsRunning())
policy_timer_.Stop();
}
void CdmEngine::OnTimerEvent() {
for (CdmSessionIter iter = sessions_.begin();
iter != sessions_.end(); ++iter) {
iter->second->OnTimerEvent();
}
}
} // namespace wvcdm

View File

@@ -0,0 +1,144 @@
// Copyright 2012 Google Inc. All Rights Reserved.
// Author: jfore@google.com (Jeff Fore), rkuroiwa@google.com (Rintaro Kuroiwa)
#include "cdm_session.h"
#include <iostream>
#include "crypto_engine.h"
#include "log.h"
#include "string_conversions.h"
#include "clock.h"
#include "wv_cdm_constants.h"
namespace wvcdm {
typedef std::set<WvCdmEventListener*>::iterator CdmEventListenerIter;
bool CdmSession::Init() {
CryptoEngine* crypto_engine = CryptoEngine::GetInstance();
if (!crypto_engine) {
LOGE("CdmSession::Init failed to get CryptoEngine instance.");
return false;
}
crypto_session_ = crypto_engine->CreateSession(session_id_);
if (!crypto_session_) {
return false;
}
std::string token;
if (!crypto_engine->GetToken(&token)) return false;
return license_parser_.Init(token, crypto_session_);
}
bool CdmSession::DestroySession() {
if (crypto_session_) {
delete crypto_session_;
crypto_session_ = NULL;
}
return true;
}
bool CdmSession::VerifySession(const CdmKeySystem& key_system,
const CdmInitData& init_data) {
// TODO(gmorgan): Compare key_system and init_data with value received
// during session startup - they should be the same.
return true;
}
CdmResponseType CdmSession::GenerateKeyRequest(const CdmInitData& init_data,
CdmKeyMessage* key_request) {
crypto_session_->Open();
if(!license_parser_.PrepareKeyRequest(init_data, key_request)) {
return KEY_ERROR;
} else {
return KEY_MESSAGE;
}
}
// AddKey() - Accept license response and extract key info.
CdmResponseType CdmSession::AddKey(const CdmKeyResponse& key_response) {
if (!license_parser_.HandleKeyResponse(key_response)) {
return KEY_ERROR;
} else {
return KEY_ADDED;
}
}
// CancelKeyRequest() - Cancel session.
CdmResponseType CdmSession::CancelKeyRequest() {
// TODO(gmorgan): cancel and clean up session
crypto_session_->Close();
return NO_ERROR;
}
// Decrypt() - Accept encrypted buffer and return decrypted data.
CdmResponseType CdmSession::Decrypt(const uint8_t* encrypted_buffer,
size_t encrypted_size,
size_t block_offset,
const std::string& iv,
const KeyId& key_id,
uint8_t* decrypted_buffer) {
return UNKNOWN_ERROR;
}
// License renewal
// GenerateRenewalRequest() - Construct valid renewal request for the current
// session keys.
CdmResponseType CdmSession::GenerateRenewalRequest(CdmKeyMessage* key_request) {
if(!license_parser_.PrepareKeyRenewalRequest(key_request)) {
return KEY_ERROR;
} else {
return KEY_MESSAGE;
}
}
// RenewKey() - Accept renewal response and update key info.
CdmResponseType CdmSession::RenewKey(const CdmKeyResponse& key_response) {
if (!license_parser_.HandleKeyRenewalResponse(key_response)) {
return KEY_ERROR;
} else {
return KEY_ADDED;
}
}
bool CdmSession::IsKeyValid(const KeyId& key_id) {
// TODO(gmorgan): lookup key and determine if valid.
// return (session_keys_.find(key_id) != session_keys_.end());
return true;
}
CdmSessionId CdmSession::GenerateSessionId() {
static const std::string kSessionPrefix("Session");
static int session_num = 1;
// TODO(rkuroiwa): Want this to be unique. Probably doing Hash(time+init_data)
// to get something that is reasonably unique.
return kSessionPrefix + IntToString(++session_num);
}
bool CdmSession::AttachEventListener(WvCdmEventListener* listener) {
std::pair<CdmEventListenerIter, bool> result = listeners_.insert(listener);
return result.second;
}
bool CdmSession::DetachEventListener(WvCdmEventListener* listener) {
return (listeners_.erase(listener) == 1);
}
void CdmSession::OnTimerEvent() {
bool event_occurred = false;
CdmEventType event;
policy_engine_.OnTimerEvent(GetCurrentTime(), event_occurred, event);
if (event_occurred) {
for (CdmEventListenerIter iter = listeners_.begin();
iter != listeners_.end(); ++iter) {
(*iter)->onEvent(session_id(), event);
}
}
}
} // namespace wvcdm

View File

@@ -0,0 +1,178 @@
// Copyright 2012 Google Inc. All Rights Reserved.
//
// Crypto - wrapper classes for OEMCrypto interface
//
#include "crypto_engine.h"
#include <iostream>
#include "log.h"
#include "OEMCryptoCENC.h"
#include "wv_cdm_constants.h"
namespace wvcdm {
CryptoEngine* CryptoEngine::crypto_engine_ = NULL;
Lock CryptoEngine::crypto_engine_lock_;
// wrapper classes for OEMCrypto interface
// CryptoEngine -- top-level interface
// CryptoSession -- session-specific interface
// CryptoKey -- key interface
// CryptoEngine methods
CryptoEngine::CryptoEngine() : initialized_(false) {}
CryptoEngine::~CryptoEngine() {
if (initialized_) {
Terminate();
}
CryptoSessionMap::iterator i(sessions_.begin());
for (; i != sessions_.end(); ++i)
delete i->second;
sessions_.clear();
}
// get the instance of OEMCrypto Client
CryptoEngine* CryptoEngine::GetInstance() {
if (NULL == crypto_engine_) {
crypto_engine_ = CreateSingleton();
}
return crypto_engine_;
}
CryptoEngine* CryptoEngine::CreateSingleton() {
AutoLock auto_lock(crypto_engine_lock_);
if (NULL == crypto_engine_) {
crypto_engine_ = new CryptoEngine;
}
return crypto_engine_;
}
void CryptoEngine::DeleteInstance() {
if (NULL != crypto_engine_) {
delete crypto_engine_;
LOGV("CryptoEngine::DeleteInstance");
crypto_engine_ = NULL;
}
}
bool CryptoEngine::Init() {
LOGV("CryptoEngine::Init: Lock");
AutoLock auto_lock(crypto_lock_);
if (!initialized_) {
OEMCryptoResult result = OEMCrypto_Initialize();
initialized_ = (OEMCrypto_SUCCESS == result);
}
return initialized_;
}
bool CryptoEngine::Terminate() {
DestroySessions();
LOGV("CryptoEngine::Terminate: Lock");
AutoLock auto_lock(crypto_lock_);
OEMCryptoResult result = OEMCrypto_Terminate();
if (OEMCrypto_SUCCESS == result) {
initialized_ = false;
}
return !initialized_;
}
bool CryptoEngine::ValidateKeybox() {
LOGV("CryptoEngine::ValidateKeybox: Lock");
AutoLock auto_lock(crypto_lock_);
OEMCryptoResult result = OEMCrypto_IsKeyboxValid();
return (OEMCrypto_SUCCESS == result);
}
CryptoSession* CryptoEngine::CreateSession(const CdmSessionId& session_id) {
LOGV("CryptoEngine::CreateSession: SLock");
AutoLock auto_lock(sessions_lock_);
if (0 == sessions_.size()) {
if (!Init()) {
return NULL;
}
}
CryptoSessionMap::iterator it = sessions_.find(session_id);
if (it != sessions_.end()) {
LOGE("CryptoEngine::CreateSession : Duplicate session ID.");
return NULL;
}
CryptoSession* new_session = new CryptoSession(session_id);
if (!new_session) {
return NULL;
}
if (!new_session->Open()) {
delete new_session;
return NULL;
}
sessions_[session_id] = new_session;
return new_session;
}
CryptoSession* CryptoEngine::FindSessionInternal(
const CdmSessionId& session_id) {
// must hold sessions_lock_
CryptoSessionMap::iterator it = sessions_.find(session_id);
if (it != sessions_.end()) {
return it->second;
}
return NULL;
}
CryptoSession* CryptoEngine::FindSession(const CdmSessionId& session_id) {
LOGV("CryptoEngine::FindSession: SLock");
AutoLock auto_lock(sessions_lock_);
return FindSessionInternal(session_id);
}
bool CryptoEngine::DestroySession(const CdmSessionId& session_id) {
LOGV("CryptoEngine::DestroySession: SLock");
AutoLock auto_lock(sessions_lock_);
if (0 == sessions_.size()) {
return false;
}
CryptoSession* session = FindSessionInternal(session_id);
if (session) {
delete session;
sessions_.erase(session_id);
return true;
} else {
return false;
}
}
bool CryptoEngine::DestroySessions() {
for (CryptoSessionMap::iterator it = sessions_.begin();
it != sessions_.end(); ++it) {
delete it->second;
}
sessions_.clear();
return true;
}
bool CryptoEngine::GetToken(std::string* token) {
LOGV("CryptoEngine::GetToken: Lock");
AutoLock auto_lock(crypto_lock_);
if (!token) {
LOGE("CryptoEngine::GetToken : No token passed to method.");
return false;
}
uint8_t buf[72];
size_t buflen = 72;
OEMCryptoResult sts = OEMCrypto_GetKeyData(buf, &buflen);
if (OEMCrypto_SUCCESS != sts) {
return false;
}
token->assign((const char*)buf, (size_t)buflen);
return true;
}
}; // namespace wvcdm

View File

@@ -0,0 +1,338 @@
// Copyright 2012 Google Inc. All Rights Reserved.
//
// Crypto - wrapper classes for OEMCrypto interface
//
#include "crypto_session.h"
#include <iostream>
#include "crypto_engine.h"
#include "log.h"
// TODO(gmorgan,jtinker): decide if OEMCryptoCENC is needed here.
#include "OEMCryptoCENC.h"
#include "string_conversions.h"
#include "wv_cdm_constants.h"
namespace {
// Encode unsigned integer into a big endian formatted string
std::string EncodeUint32(unsigned int u) {
std::string s;
s.append(1, (u >> 24) & 0xFF);
s.append(1, (u >> 16) & 0xFF);
s.append(1, (u >> 8) & 0xFF);
s.append(1, (u >> 0) & 0xFF);
return s;
}
}
namespace wvcdm {
// wrapper classes for OEMCrypto interface
// CryptoEngine -- top-level interface
// CryptoSession -- session-specific interface
// CryptoKey -- key interface
// CryptoSession methods
CryptoSession::CryptoSession() : valid_(false), open_(false) {}
CryptoSession::CryptoSession(const std::string& sname) : valid_(true),
open_(false), cdm_session_id_(sname) {}
CryptoSession::~CryptoSession() {
if (open_) {
Close();
}
LOGV("CryptoSession::dtor: SLock");
CryptoEngine* crypto_engine = CryptoEngine::GetInstance();
crypto_engine->sessions_.erase(cdm_session_id_);
if (0 == crypto_engine->sessions_.size()) {
crypto_engine->DeleteInstance();
}
CryptoKeyMap::iterator i(keys_.begin());
for (; i != keys_.end(); ++i)
delete i->second;
keys_.clear();
}
bool CryptoSession::Open() {
LOGV("CryptoSession::Open: Lock");
CryptoEngine* crypto_engine = CryptoEngine::GetInstance();
AutoLock auto_lock(crypto_engine->crypto_lock_);
OEMCrypto_SESSION sid;
OEMCryptoResult sts;
if (open_)
return false;
sts = OEMCrypto_OpenSession(&sid);
if (OEMCrypto_SUCCESS != sts) {
open_ = false;
} else {
oec_session_id_ = static_cast<CryptoSessionId>(sid);
LOGV("OpenSession: id= %ld", (uint32_t) oec_session_id_);
open_ = true;
}
return open_;
}
void CryptoSession::Close() {
LOGV("CryptoSession::Close: Lock");
CryptoEngine* crypto_engine = CryptoEngine::GetInstance();
AutoLock auto_lock(crypto_engine->crypto_lock_);
LOGV("CloseSession: id=%ld open=%s", (uint32_t) oec_session_id_, open_? "true" : "false") ;
if (open_) {
OEMCryptoResult sts = OEMCrypto_CloseSession(oec_session_id_);
if (OEMCrypto_SUCCESS == sts) {
open_ = false;
}
}
}
void CryptoSession::GenerateRequestId(std::string& req_id_str) {
LOGV("CryptoSession::GenerateRequestId: Lock");
CryptoEngine* crypto_engine = CryptoEngine::GetInstance();
AutoLock auto_lock(crypto_engine->crypto_lock_);
// TODO(gmorgan): Get unique ID from OEMCrypto
req_id_str.assign("987654321");
}
bool CryptoSession::PrepareRequest(const std::string& message,
std::string* signature) {
LOGV("CryptoSession::PrepareRequest: Lock");
CryptoEngine* crypto_engine = CryptoEngine::GetInstance();
AutoLock auto_lock(crypto_engine->crypto_lock_);
if (!signature) {
LOGE("CryptoSession::PrepareRequest : No output destination provided.");
return false;
}
uint8_t signature_buf[32];
size_t length = 32;
OEMCryptoResult sts;
std::string mac_deriv_message;
std::string enc_deriv_message;
GenerateMacContext(message, &mac_deriv_message);
GenerateEncryptContext(message, &enc_deriv_message);
LOGV("GenerateDerivedKeys: id=%ld", (uint32_t) oec_session_id_);
sts = OEMCrypto_GenerateDerivedKeys(
oec_session_id_,
reinterpret_cast<const uint8_t*>(mac_deriv_message.data()),
mac_deriv_message.size(),
reinterpret_cast<const uint8_t*>(enc_deriv_message.data()),
enc_deriv_message.size());
if (OEMCrypto_SUCCESS != sts) {
return false;
}
LOGV("GenerateSignature: id=%ld", (uint32_t) oec_session_id_);
sts = OEMCrypto_GenerateSignature(
oec_session_id_,
reinterpret_cast<const uint8_t*>(message.data()),
message.size(),
signature_buf,
&length);
if (OEMCrypto_SUCCESS != sts) {
return false;
}
signature->assign(reinterpret_cast<const char*>(signature_buf), length);
return true;
}
bool CryptoSession::PrepareRenewalRequest(const std::string& message,
std::string* signature) {
LOGV("CryptoSession::PrepareRenewalRequest: Lock");
CryptoEngine* crypto_engine = CryptoEngine::GetInstance();
AutoLock auto_lock(crypto_engine->crypto_lock_);
uint8_t signature_buf[32];
size_t length = 32;
OEMCryptoResult sts = OEMCrypto_GenerateSignature(
oec_session_id_, reinterpret_cast<const uint8_t*>(message.data()),
message.size(), signature_buf, &length);
if (OEMCrypto_SUCCESS != sts) {
return false;
}
signature->assign(reinterpret_cast<const char*>(signature_buf), length);
return true;
}
void CryptoSession::GenerateMacContext(const std::string& input_context,
std::string* deriv_context) {
if (!deriv_context) {
LOGE("CryptoSession::GenerateMacContext : No output destination provided.");
return;
}
const std::string kSigningKeyLabel = "AUTHENTICATION";
const size_t kSigningKeySizeBits = MAC_KEY_SIZE * 8;
deriv_context->assign(kSigningKeyLabel);
deriv_context->append(1, '\0');
deriv_context->append(input_context);
deriv_context->append(EncodeUint32(kSigningKeySizeBits));
}
void CryptoSession::GenerateEncryptContext(const std::string& input_context,
std::string* deriv_context) {
if (!deriv_context) {
LOGE("CryptoSession::GenerateEncryptContext : No output destination provided.");
return;
}
const std::string kEncryptionKeyLabel = "ENCRYPTION";
const size_t kEncryptionKeySizeBits = KEY_SIZE * 8;
deriv_context->assign(kEncryptionKeyLabel);
deriv_context->append(1, '\0');
deriv_context->append(input_context);
deriv_context->append(EncodeUint32(kEncryptionKeySizeBits));
}
size_t CryptoSession::GetOffset(std::string message, std::string field) {
size_t pos = message.find(field);
if (pos == std::string::npos) {
LOGE("CryptoSession::GetOffset : Cannot find offset for %s", field.c_str());
pos = 0;
}
return pos;
}
bool CryptoSession::LoadKeys(const std::string& message,
const std::string& signature,
const std::string& mac_key_iv,
const std::string& mac_key,
int num_keys,
const CryptoKey* key_array) {
LOGV("CryptoSession::LoadKeys: Lock");
CryptoEngine* crypto_engine = CryptoEngine::GetInstance();
AutoLock auto_lock(crypto_engine->crypto_lock_);
const uint8_t* msg = reinterpret_cast<const uint8_t*>(message.data());
const uint8_t* enc_mac_key = NULL;
const uint8_t* enc_mac_key_iv = NULL;
if (mac_key.size() >= MAC_KEY_SIZE && mac_key_iv.size() >= KEY_IV_SIZE) {
enc_mac_key = msg + GetOffset(message, mac_key);
enc_mac_key_iv = msg + GetOffset(message, mac_key_iv);
}
OEMCrypto_KeyObject load_key_array[num_keys];
for (int i=0; i<num_keys; ++i) {
const CryptoKey* ki = &key_array[i];
OEMCrypto_KeyObject* ko = &load_key_array[i];
ko->key_id = msg + GetOffset(message, ki->key_id());
ko->key_id_length = ki->key_id().length();
ko->key_data_iv = msg + GetOffset(message, ki->key_data_iv());
ko->key_data = msg + GetOffset(message, ki->key_data());
if (ki->HasKeyControl()) {
ko->key_control_iv = msg + GetOffset(message, ki->key_control_iv());
ko->key_control = msg + GetOffset(message, ki->key_control());
}
else {
LOGE("For key %d: XXX key has no control block. size=%d", i, ki->key_control().size());
ko->key_control_iv = NULL;
ko->key_control = NULL;
}
}
LOGV("LoadKeys: id=%ld", (uint32_t) oec_session_id_);
return (OEMCrypto_SUCCESS == OEMCrypto_LoadKeys(
oec_session_id_, msg, message.size(),
reinterpret_cast<const uint8_t*>(signature.data()),
signature.size(), enc_mac_key_iv, enc_mac_key,
num_keys, load_key_array));
}
bool CryptoSession::RefreshKeys(const std::string& message,
const std::string& signature,
int num_keys,
const CryptoKey* key_array) {
LOGV("CryptoSession::RefreshKeys: Lock");
CryptoEngine* crypto_engine = CryptoEngine::GetInstance();
AutoLock auto_lock(crypto_engine->crypto_lock_);
const uint8_t* msg = reinterpret_cast<const uint8_t*>(message.data());
OEMCrypto_KeyRefreshObject load_key_array[num_keys];
for (int i=0; i<num_keys; ++i) {
const CryptoKey* ki = &key_array[i];
OEMCrypto_KeyRefreshObject* ko = &load_key_array[i];
if (ki->key_id().empty()) {
ko->key_id = NULL;
} else {
ko->key_id = msg + GetOffset(message, ki->key_id());
}
if (ki->HasKeyControl()) {
if (ki->key_control_iv().empty()) {
ko->key_control_iv = NULL;
} else {
ko->key_control_iv = msg + GetOffset(message, ki->key_control_iv());
}
ko->key_control = msg + GetOffset(message, ki->key_control());
}
else {
ko->key_control_iv = NULL;
ko->key_control = NULL;
}
}
LOGV("RefreshKeys: id=%ld", static_cast<uint32_t>(oec_session_id_));
return (OEMCrypto_SUCCESS == OEMCrypto_RefreshKeys(
oec_session_id_, msg, message.size(),
reinterpret_cast<const uint8_t*>(signature.data()),
signature.size(),
num_keys, load_key_array));
}
bool CryptoSession::SelectKey(const std::string& key_id) {
LOGV("CryptoSession::SelectKey: Lock");
CryptoEngine* crypto_engine = CryptoEngine::GetInstance();
AutoLock auto_lock(crypto_engine->crypto_lock_);
const uint8_t* key_id_string =
reinterpret_cast<const uint8_t*>(key_id.data());
LOGV("SelectKey: id=%ld", static_cast<uint32_t>(oec_session_id_));
OEMCryptoResult sts = OEMCrypto_SelectKey(oec_session_id_, key_id_string,
key_id.size());
if (OEMCrypto_SUCCESS != sts) {
return false;
}
return true;
}
bool CryptoSession::Decrypt(const InputDescriptor input,
const OutputDescriptor output) {
LOGV("CryptoSession::Decrypt: Lock");
CryptoEngine* crypto_engine = CryptoEngine::GetInstance();
AutoLock auto_lock(crypto_engine->crypto_lock_);
// TODO(gmorgan): handle inputs and outputs to decrypt call
const uint8_t* data_addr = NULL;
uint32_t data_length = 0;
bool is_encrypted = false;
uint8_t* iv = NULL;
uint32_t offset = 0;
const OEMCrypto_DestBufferDesc* out_buffer = NULL;
OEMCryptoResult sts = OEMCrypto_DecryptCTR(oec_session_id_, data_addr,
data_length, is_encrypted, iv,
offset, out_buffer);
if (OEMCrypto_SUCCESS != sts) {
return false;
}
return true;
}
bool CryptoSession::GenerateNonce(uint32_t* nonce) {
if (!nonce) {
LOGE("input parameter is null");
return false;
}
LOGV("CryptoSession::GenerateNonce: Lock");
CryptoEngine* crypto_engine = CryptoEngine::GetInstance();
AutoLock auto_lock(crypto_engine->crypto_lock_);
return(OEMCrypto_SUCCESS == OEMCrypto_GenerateNonce(oec_session_id_, nonce));
}
}; // namespace wvcdm

View File

@@ -0,0 +1,298 @@
// Copyright 2012 Google Inc. All Rights Reserved.
#include "license.h"
#include "crypto_session.h"
#include "log.h"
#include "string_conversions.h"
#include "wv_cdm_constants.h"
namespace wvcdm {
// Protobuf generated classes.
using video_widevine_server::sdk::LicenseRequest;
using video_widevine_server::sdk::LicenseRequest_ClientIdentification;
using video_widevine_server::sdk::LicenseRequest_ClientIdentification_NameValue;
using video_widevine_server::sdk::LicenseRequest_ContentIdentification;
using video_widevine_server::sdk::LicenseRequest_ContentIdentification_CENC;
using video_widevine_server::sdk::License;
using video_widevine_server::sdk::License_KeyContainer;
using video_widevine_server::sdk::SignedMessage;
using video_widevine_server::sdk::STREAMING;
using video_widevine_server::sdk::LicenseRequest_ContentIdentification_ExistingLicense;
CdmLicense::CdmLicense(): session_(NULL) {}
CdmLicense::~CdmLicense() {}
bool CdmLicense::Init(const std::string& token, CryptoSession* session) {
if (token.size() == 0)
return false;
if (session == NULL || !session->IsValid() || !session->IsOpen())
return false;
token_ = token;
session_ = session;
return true;
}
bool CdmLicense::PrepareKeyRequest(const CdmInitData& init_data,
CdmKeyMessage* signed_request) {
if (!session_ ||
token_.empty()) {
return false;
}
if (init_data.empty()) {
LOGE("CdmLicense::PrepareKeyRequest : No init data provided;");
return false;
}
if (!signed_request) {
LOGE("CdmLicense::PrepareKeyRequest : No signed request provided.");
return false;
}
// TODO(gmorgan): Request ID owned by session?
std::string request_id;
session_->GenerateRequestId(request_id);
LicenseRequest license_request;
LicenseRequest_ClientIdentification* client_id =
license_request.mutable_client_id();
client_id->set_type(LicenseRequest_ClientIdentification::KEYBOX);
client_id->set_token(token_);
// Content Identification may be a cenc_id, a webm_id or a license_id
LicenseRequest_ContentIdentification* content_id =
license_request.mutable_content_id();
LicenseRequest_ContentIdentification_CENC* cenc_content_id =
content_id->mutable_cenc_id();
cenc_content_id->add_pssh(init_data);
cenc_content_id->set_license_type(STREAMING);
cenc_content_id->set_request_id(request_id);
// TODO(jfore): The time field will be updated once the cdm wrapper
// has been updated to pass us in the time.
license_request.set_request_time(0);
license_request.set_type(LicenseRequest::NEW);
// Get/set the nonce. This value will be reflected in the Key Control Block
// of the license response.
uint32_t nonce;
if (!session_->GenerateNonce(&nonce)) {
return false;
}
license_request.set_key_control_nonce(UintToString(nonce));
LOGD("PrepareKeyRequest: nonce=%u", nonce);
// License request is complete. Serialize it.
std::string serialized_license_req;
license_request.SerializeToString(&serialized_license_req);
// Derive signing and encryption keys and construct signature.
std::string license_request_signature;
if (!session_->PrepareRequest(serialized_license_req,
&license_request_signature)) {
signed_request->clear();
return false;
}
if (license_request_signature.empty()) {
signed_request->clear();
return false;
}
// Put serialize license request and signature together
SignedMessage signed_message;
signed_message.set_type(SignedMessage::LICENSE_REQUEST);
signed_message.set_signature(license_request_signature);
signed_message.set_msg(serialized_license_req);
signed_message.SerializeToString(signed_request);
return true;
}
bool CdmLicense::PrepareKeyRenewalRequest(CdmKeyMessage* signed_request) {
if (!session_) {
return false;
}
if (!signed_request) {
LOGE("CdmLicense::PrepareKeyRenewalRequest : No signed request provided.");
return false;
}
LicenseRequest license_request;
license_request.set_type(LicenseRequest::RENEWAL);
LicenseRequest_ContentIdentification_ExistingLicense* current_license =
license_request.mutable_content_id()->mutable_license();
current_license->mutable_license_id()->CopyFrom(license_id_);
// Get/set the nonce. This value will be reflected in the Key Control Block
// of the license response.
uint32_t nonce;
if (!session_->GenerateNonce(&nonce)) {
return false;
}
license_request.set_key_control_nonce(UintToString(nonce));
LOGD("PrepareKeyRenewalRequest: nonce=%u", nonce);
// License request is complete. Serialize it.
std::string serialized_license_req;
license_request.SerializeToString(&serialized_license_req);
// Construct signature.
std::string license_request_signature;
if (!session_->PrepareRenewalRequest(serialized_license_req,
&license_request_signature))
return false;
if (license_request_signature.empty()) return false;
// Put serialize license request and signature together
SignedMessage signed_message;
signed_message.set_type(SignedMessage::LICENSE_REQUEST);
signed_message.set_signature(license_request_signature);
signed_message.set_msg(serialized_license_req);
signed_message.SerializeToString(signed_request);
return true;
}
bool CdmLicense::HandleKeyResponse(const CdmKeyResponse& license_response) {
if (!session_) {
return false;
}
if (license_response.empty()) {
LOGE("CdmLicense::HandleKeyResponse : Empty license response.");
return false;
}
SignedMessage signed_response;
if (!signed_response.ParseFromString(license_response))
return false;
if (!signed_response.has_signature())
return false;
License license;
if (!license.ParseFromString(signed_response.msg()))
return false;
// Extract mac key
std::string mac_key_iv;
std::string mac_key;
if (license.policy().can_renew())
{
for (int i = 0; i < license.key_size(); ++i) {
if (license.key(i).type() == License_KeyContainer::SIGNING) {
mac_key_iv.assign(license.key(i).iv());
// Strip off PKCS#5 padding
mac_key.assign(license.key(i).key().data(), MAC_KEY_SIZE);
}
}
if (mac_key_iv.size() != KEY_IV_SIZE ||
mac_key.size() != MAC_KEY_SIZE)
{
return false;
}
// License Id should not be empty for renewable license
if (!license.has_id()) return false;
license_id_.CopyFrom(license.id());
}
CryptoKey* key_array = new CryptoKey[license.key_size()];
if (key_array == NULL) return false;
// Extract content key(s)
int num_keys = 0;
for (int i = 0; i < license.key_size(); ++i) {
// TODO(kqyang): Key ID size is not fixed in spec, but conventionally we
// always use 16 bytes key id. We'll need to update oemcrypto to support
// variable size key id.
if (license.key(i).id().size() == KEY_ID_SIZE &&
license.key(i).key().size() == KEY_SIZE + KEY_PAD_SIZE &&
license.key(i).type() == License_KeyContainer::CONTENT) {
key_array[num_keys].set_key_id(license.key(i).id());
// Strip off PKCS#5 padding
key_array[num_keys].set_key_data(
license.key(i).key().substr(0, KEY_SIZE));
key_array[num_keys].set_key_data_iv(license.key(i).iv());
if (license.key(i).has_key_control()) {
key_array[num_keys].set_key_control(
license.key(i).key_control().struct_());
key_array[num_keys].set_key_control_iv(
license.key(i).key_control().iv());
}
num_keys++;
}
}
if (num_keys == 0) return false;
// TODO(kqyang): move protocol buffer related stuff in policy
// engine to this file.
// policy_engine_.SetLicense(license);
bool status = session_->LoadKeys(signed_response.msg(),
signed_response.signature(),
mac_key_iv,
mac_key,
num_keys,
key_array);
delete[] key_array;
return status;
}
bool CdmLicense::HandleKeyRenewalResponse(
const CdmKeyResponse& license_response) {
if (!session_) {
return false;
}
if (license_response.empty()) {
LOGE("CdmLicense::HandleKeyRenewalResponse : Empty license response.");
return false;
}
SignedMessage signed_response;
if (!signed_response.ParseFromString(license_response))
return false;
if (!signed_response.has_signature())
return false;
// TODO(jfore): refresh the keys in oemcrypto
License license;
if (!license.ParseFromString(signed_response.msg()))
return false;
if (!license.has_id()) return false;
if (license.id().version() > license_id_.version()) {
//This is the normal case.
license_id_.CopyFrom(license.id());
// TODO(kqyang): should we move protocol buffer related stuff in policy
// engine to this file instead?
// policy_engine_.UpdateLicense(license);
} else {
// This isn't supposed to happen.
// TODO(jfore): Handle wrap? We can miss responses and that should be
// considered normal until retries are exhausted.
// policy_.set_can_play(false);
}
return true;
}
} // namespace wvcdm

View File

@@ -0,0 +1,224 @@
syntax = "proto2";
package video_widevine_server.sdk;
option optimize_for = LITE_RUNTIME;
enum LicenseType {
STREAMING = 1;
OFFLINE = 2;
}
// LicenseIdentification is propagated from LicenseRequest to License,
// incrementing version with each iteration.
message LicenseIdentification {
optional bytes request_id = 1;
optional bytes session_id = 2;
optional bytes purchase_id = 3;
optional LicenseType type = 4;
optional int32 version = 5;
}
message License {
message Policy {
// Indicates that playback of the content is allowed.
optional bool can_play = 1 [default = false];
// Indicates that the license may be persisted to non-volatile
// storage for offline use.
optional bool can_persist = 2 [default = false];
// Indicates that renewal of this license is allowed.
optional bool can_renew = 3 [default = false];
// For the |*duration*| fields, playback must halt when
// license_start_time (seconds since the epoch (UTC)) +
// license_duration_seconds is exceeded. A value of 0
// indicates that there is no limit to the duration.
// Indicates the rental window.
optional int64 rental_duration_seconds = 4 [default = 0];
// Indicates the viewing window, once playback has begun.
optional int64 playback_duration_seconds = 5 [default = 0];
// Indicates the time window for this specific license.
optional int64 license_duration_seconds = 6 [default = 0];
// The |renewal*| fields only apply if |can_renew| is true.
// The window of time, in which playback is allowed to continue while
// renewal is attempted, yet unsuccessful due to backend problems with
// the license server.
optional int64 renewal_recovery_duration_seconds = 7 [default = 0];
// All renewal requests for this license shall be directed to the
// specified URL.
optional string renewal_server_url = 8;
// How many seconds after license_start_time, before renewal is first
// attempted.
optional int64 renewal_delay_seconds = 9 [default = 0];
// Specifies the delay in seconds between subsequent license
// renewal requests, in case of failure.
optional int64 renewal_retry_interval_seconds = 10 [default = 0];
// Indicates that the license shall be sent for renewal when usage is
// started.
optional bool renew_with_usage = 11 [default = false];
}
message KeyContainer {
enum KeyType {
// Exactly one key of this type must appear.
SIGNING = 1;
CONTENT = 2;
}
// The SecurityLevel enumeration allows the server to communicate the level
// of robustness required by the client, in order to use the key.
enum SecurityLevel {
// Software-based whitebox crypto is required.
SW_SECURE_CRYPTO = 1;
// Software crypto and an obfuscated decoder is required.
SW_SECURE_DECODE = 2;
// The key material and crypto operations must be performed within a
// hardware backed trusted execution environment.
HW_SECURE_CRYPTO = 3;
// The crypto and decoding of content must be performed within a hardware
// backed trusted execution environment.
HW_SECURE_DECODE = 4;
// The crypto, decoding and all handling of the media (compressed and
// uncompressed) must be handled within a hardware backed trusted
// execution environment.
HW_SECURE_ALL = 5;
}
message KeyControl {
// |key_control| is documented here:
// https://docs.google.com/a/google.com/document/d/17eDxzzGpPc2qSm7zW68_5ensuxbHErYCvD3IxSKETRo/edit#
// If present, the key control must be communicated to the secure
// environment prior to any usage.
optional bytes struct = 1;
optional bytes iv = 2;
}
message OutputProtection {
// Indicates whether HDCP is required on digital outputs, and which
// version should be used.
enum HDCP {
HDCP_NONE = 0;
HDCP_V1 = 1;
HDCP_V2 = 2;
}
optional HDCP hdcp = 1 [default = HDCP_NONE];
// Indicate the CGMS setting to be inserted on analog output.
enum CGMS {
CGMS_NONE = 42;
COPY_FREE = 0;
COPY_ONCE = 2;
COPY_NEVER = 3;
}
optional CGMS cgms_flags = 2 [default = CGMS_NONE];
}
optional bytes id = 1;
optional bytes iv = 2;
optional bytes key = 3;
optional KeyType type = 4;
optional SecurityLevel level = 5 [default = SW_SECURE_CRYPTO];
optional OutputProtection required_protection = 6;
optional OutputProtection requested_protection = 7;
optional KeyControl key_control = 8;
}
optional LicenseIdentification id = 1;
optional Policy policy = 2;
repeated KeyContainer key = 3;
optional int64 license_start_time = 4;
}
message LicenseRequest {
message ClientIdentification {
enum TokenType {
KEYBOX = 0;
}
message NameValue {
optional string name = 1;
optional string value = 2;
}
optional TokenType type = 1;
optional bytes token = 2;
repeated NameValue client_info = 3;
}
message ContentIdentification {
message CENC {
repeated bytes pssh = 1;
optional LicenseType license_type = 2;
optional bytes request_id = 3; // Opaque, client-specified.
}
message WebM {
optional bytes header = 1;
optional LicenseType license_type = 2;
optional bytes request_id = 3; // Opaque, client-specified.
}
message ExistingLicense {
optional LicenseIdentification license_id = 1;
optional int64 seconds_since_started = 2;
}
// Exactly one of these must be present.
optional CENC cenc_id = 1;
optional WebM webm_id = 2;
optional ExistingLicense license = 3;
}
enum RequestType {
NEW = 1;
RENEWAL = 2;
RELEASE = 3;
}
optional ClientIdentification client_id = 1;
optional ContentIdentification content_id = 2;
optional RequestType type = 3;
optional int64 request_time = 4;
optional bytes key_control_nonce = 5;
}
message SignedMessage {
enum MessageType {
LICENSE_REQUEST = 1;
LICENSE = 2;
}
optional MessageType type = 1;
optional bytes msg = 2;
optional bytes signature = 3;
}
// This message is used to pass optional data on initial license issuance.
message SessionInit {
optional string session_id = 1;
optional string purchase_id = 2;
optional string master_signing_key = 3;
optional string signing_key = 4;
optional int64 license_start_time = 5;
}
// This message is used by the server to preserve and restore session state.
message SessionState {
optional LicenseIdentification license_id = 1;
optional bytes signing_key = 2;
optional uint32 keybox_system_id = 3;
}

View File

@@ -0,0 +1,211 @@
// Copyright 2013 Google Inc. All Rights Reserved.
#include "policy_engine.h"
#include <algorithm>
#include <map>
#include <string>
#include <vector>
#include "log.h"
#include "string_conversions.h"
namespace wvcdm {
PolicyEngine::PolicyEngine() :
license_state_(kLicenseStateInitial),
license_start_time_(0),
next_renewal_time_(0),
policy_max_duration_seconds_(0) {
}
PolicyEngine::~PolicyEngine() {
}
void PolicyEngine::OnTimerEvent(int64_t current_time, bool event_occured, CdmEventType& event) {
event_occured = false;
// License expiration trumps all.
if (IsLicenseDurationExpired(current_time) &&
license_state_ != kLicenseStateExpired) {
license_state_ = kLicenseStateExpired;
event = LICENSE_EXPIRED_EVENT;
event_occured = true;
return;
}
// Test to determine if renewal should be attempted.
switch (license_state_) {
case kLicenseStateCanPlay: {
if (IsRenewalDelayExpired(current_time)) {
license_state_ = kLicenseStateNeedRenewal;
UpdateRenewalRequest(current_time);
event = LICENSE_RENEWAL_NEEDED_EVENT;
event_occured = true;
}
return;
}
case kLicenseStateNeedRenewal: {
UpdateRenewalRequest(current_time);
event = LICENSE_RENEWAL_NEEDED_EVENT;
event_occured = true;
return;
}
case kLicenseStateWaitingLicenseUpdate: {
if (IsRenewalRetryIntervalExpired(current_time)) {
UpdateRenewalRequest(current_time);
event = LICENSE_RENEWAL_NEEDED_EVENT;
event_occured = true;
}
return;
}
case kLicenseStateInitial:
case kLicenseStateExpired: {
return;
}
default: {
license_state_ = kLicenseStateCannotPlay;
return;
}
}
}
// TODO: Fix up differences between Eureka and other platforms' use cases.
// Eureka does not get calls to decrypt. license usage begins
// when we receive the initial license. renew_with_usage will cause renewal to
// occur on the first call to OnTimeEvent after PolicyEngine::SetLicense is
// called.
void PolicyEngine::SetLicense(
const video_widevine_server::sdk::License& license) {
license_id_.Clear();
license_id_.CopyFrom(license.id());
policy_.Clear();
UpdateLicense(license);
}
void PolicyEngine::UpdateLicense(
const video_widevine_server::sdk::License& license) {
if (!license.has_policy() || kLicenseStateExpired == license_state_)
return;
policy_.MergeFrom(license.policy());
policy_max_duration_seconds_ = 0;
// Calculate policy_max_duration_seconds_. policy_max_duration_seconds_
// will be set to the minimum of the following policies :
// rental_duration_seconds, playback_duration_seconds, and
// license_duration_seconds. The value is used to determine
// when the license expires.
if (policy_.has_rental_duration_seconds())
policy_max_duration_seconds_ = policy_.rental_duration_seconds();
if ((policy_.has_playback_duration_seconds() &&
(policy_.playback_duration_seconds() < policy_max_duration_seconds_)) ||
!policy_max_duration_seconds_) {
policy_max_duration_seconds_ = policy_.playback_duration_seconds();
}
if ((policy_.has_license_duration_seconds() &&
(policy_.license_duration_seconds() < policy_max_duration_seconds_)) ||
!policy_max_duration_seconds_) {
policy_max_duration_seconds_ = policy_.license_duration_seconds();
}
switch (license_state_) {
case kLicenseStateInitial: {
// Process initial license.
license_start_time_ = license.license_start_time();
if (policy_.can_play()) {
license_state_ =
policy_.renew_with_usage() ?
kLicenseStateNeedRenewal : kLicenseStateCanPlay;
} else {
license_state_ = kLicenseStateExpired;
}
next_renewal_time_ = license_start_time_
+ policy_.renewal_delay_seconds();
}
break;
case kLicenseStateExpired:
// Ignore policy updates.
return;
default: {
// Process license renewal.
if (license.id().version() > license_id_.version()) {
// This is the normal case.
policy_.MergeFrom(license.policy());
license_id_.CopyFrom(license.id());
} else {
// This isn't supposed to happen.
// TODO(jfore): Handle wrap? We can miss responses and that should be
// considered normal until retries are exhausted.
policy_.set_can_play(false);
}
if (license.has_license_start_time()) {
// license start has been updated. Transition back to the
// normal kLicenseStateCanPlay state if playback is allowed by
// the updated license.
license_start_time_ = license.license_start_time();
next_renewal_time_ = license_start_time_
+ policy_.renewal_delay_seconds();
license_state_ =
policy_.can_play() ? kLicenseStateCanPlay : kLicenseStateExpired;
} else {
// license start was not updated. Continue sending renewel requests
// at the specified retry rate. To perform this we transition directly
// to kLicenseStateWaitingLicenseUpdate. While in this state
// IsRenewalRetryIntervalExpired will always return false if retries are
// not allowed. Note that next_renewal_time_ was updated when this
// renewal was requested.
license_state_ =
policy_.can_play() ?
kLicenseStateWaitingLicenseUpdate : kLicenseStateExpired;
}
}
}
}
void PolicyEngine::UpdateRenewalRequest(int64_t current_time) {
license_state_ = kLicenseStateWaitingLicenseUpdate;
next_renewal_time_ = current_time + policy_.renewal_retry_interval_seconds();
}
// For the policy time fields checked in the following methods, a value of 0
// indicates that there is no limit to the duration. These methods
// will always return false if the value is 0.
bool PolicyEngine::IsLicenseDurationExpired(int64_t current_time) {
return policy_max_duration_seconds_ &&
license_start_time_ + policy_max_duration_seconds_ <=
current_time;
}
bool PolicyEngine::IsRenewalDelayExpired(int64_t current_time) {
return (policy_.renewal_delay_seconds() > 0) &&
license_start_time_ + policy_.renewal_delay_seconds() <=
current_time;
}
// TODO(jfore): there is some gray around how this should be
// implemented. It currently is not.
bool PolicyEngine::IsRenewalRecoveryDurationExpired(
int64_t current_time) {
return (policy_.renewal_recovery_duration_seconds() > 0) &&
license_start_time_ + policy_.renewal_recovery_duration_seconds() <=
current_time;
}
bool PolicyEngine::IsRenewalRetryIntervalExpired(
int64_t current_time) {
return (policy_.renewal_retry_interval_seconds() > 0) &&
next_renewal_time_ <= current_time;
}
} // wvcdm

View File

@@ -0,0 +1,211 @@
// Copyright 2012 Google Inc. All Rights Reserved.
#include "string_conversions.h"
#include <ctype.h>
#include <iostream>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <vector>
#include "log.h"
namespace {
// Helper for Base64SafeDecode()
char B64ToBin(char inch) {
if (inch >= 'A' && inch <= 'Z') return inch - 'A';
if (inch >= 'a' && inch <= 'z') return inch - 'a' + 26;
if (inch >= '0' && inch <= '9') return inch - '0' + 52;
if (inch == '-') return 62;
// if (inch == '_')
return 63;
}
}
namespace wvcdm {
static bool CharToDigit(char ch, unsigned char* digit) {
if (ch >= '0' && ch <= '9') {
*digit = ch - '0';
} else {
ch = tolower(ch);
if ((ch >= 'a') && (ch <= 'f')) {
*digit = ch - 'a' + 10;
} else {
return false;
}
}
return true;
}
// converts an ascii hex string(2 bytes per digit) into a decimal byte string
std::vector<uint8_t> a2b_hex(const std::string& byte) {
std::vector<uint8_t> array;
unsigned int count = byte.size();
if (count == 0 || (count % 2) != 0) {
LOGE("Invalid input size %u for string %s", count, byte.c_str());
return array;
}
for (unsigned int i = 0; i < count / 2; ++i) {
unsigned char msb = 0; // most significant 4 bits
unsigned char lsb = 0; // least significant 4 bits
if (!CharToDigit(byte[i * 2], &msb) ||
!CharToDigit(byte[i * 2 + 1], &lsb)) {
LOGE("Invalid hex value %c%c at index %d", byte[i*2], byte[i*2+1], i);
return array;
}
array.push_back((msb << 4) | lsb);
}
return array;
}
std::string a2bs_hex(const std::string& byte) {
std::vector<uint8_t> array = a2b_hex(byte);
return std::string(array.begin(), array.end());
}
std::string b2a_hex(const std::vector<uint8_t>& byte) {
return HexEncode(&byte[0], byte.size());
}
std::string b2a_hex(const std::string& byte) {
return HexEncode(reinterpret_cast<const uint8_t *>(byte.data()),
byte.length());
}
// Filename-friendly base64 encoding (RFC4648).
// This is the encoding required by GooglePlay for certain
// license server transactions. It is also used for logging
// certain strings.
std::string Base64SafeEncode(const std::vector<uint8_t>& bin_input) {
static const char kBase64Chars[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789-_";
if (bin_input.empty()) {
return std::string();
}
int in_size = bin_input.size();
int rup = ((in_size % 3) != 0) ? 1 : 0;
int out_size = ((in_size * 4) / 3) + rup;
std::string b64_output(out_size, '\0');
int in_index = 0;
int out_index = 0;
unsigned long buffer;
unsigned char out_cc;
static const unsigned long kInMask = 0xff;
static const unsigned long kOutMask = 0x3f;
while (in_index < in_size) {
// up to 3 bytes (0..255) in
buffer = (bin_input.at(in_index) & kInMask);
buffer <<= 8;
buffer |= (++in_index >= in_size) ? 0 : (bin_input.at(in_index) & kInMask);
buffer <<= 8;
buffer |= (++in_index >= in_size) ? 0 : (bin_input.at(in_index) & kInMask);
++in_index;
// up to 4 bytes (0..63) out
out_cc = (buffer >> 18) & kOutMask;
b64_output.at(out_index) = kBase64Chars[out_cc];
if (++out_index >= out_size)
break;
out_cc = (buffer >> 12) & kOutMask;
b64_output.at(out_index) = kBase64Chars[out_cc];
if (++out_index >= out_size)
break;
out_cc = (buffer >> 6) & kOutMask;
b64_output.at(out_index) = kBase64Chars[out_cc];
if (++out_index >= out_size)
break;
out_cc = buffer & kOutMask;
b64_output.at(out_index) = kBase64Chars[out_cc];
++out_index;
}
return b64_output;
}
// Decode for Filename-friendly base64 encoding (RFC4648).
// This is the encoding required by GooglePlay for certain
// license server transactions. It is also used for logging
// certain strings.
std::vector<uint8_t> Base64SafeDecode(const std::string& b64_input) {
if (b64_input.empty()) {
return std::vector<uint8_t>();
}
int in_size = b64_input.size();
// out_size should be an integral number of bytes, assuming correct encode
int out_size = ((in_size * 3) / 4);
std::vector<uint8_t> bin_output(out_size, '\0');
int in_index = 0;
int out_index = 0;
unsigned long buffer;
unsigned char out_cc;
static const unsigned long kOutMask = 0xff;
while (in_index < in_size) {
// up to 4 bytes (0..63) in
buffer = B64ToBin(b64_input.at(in_index));
buffer <<= 6;
buffer |= (++in_index >= in_size) ? 0 : B64ToBin(b64_input.at(in_index));
buffer <<= 6;
buffer |= (++in_index >= in_size) ? 0 : B64ToBin(b64_input.at(in_index));
buffer <<= 6;
buffer |= (++in_index >= in_size) ? 0 : B64ToBin(b64_input.at(in_index));
++in_index;
// up to 3 bytes (0..255) out
out_cc = (buffer >> 16) & kOutMask;
bin_output.at(out_index) = out_cc;
if (++out_index >= out_size)
break;
out_cc = (buffer >> 8) & kOutMask;
bin_output.at(out_index) = out_cc;
if (++out_index >= out_size)
break;
out_cc = buffer & kOutMask;
bin_output.at(out_index) = out_cc;
++out_index;
}
return bin_output;
}
std::string HexEncode(const uint8_t* in_buffer, unsigned int size) {
static const char kHexChars[] = "0123456789ABCDEF";
// Each input byte creates two output hex characters.
std::string out_buffer(size * 2, '\0');
for (unsigned int i = 0; i < size; ++i) {
char byte = in_buffer[i];
out_buffer[(i << 1)] = kHexChars[(byte >> 4) & 0xf];
out_buffer[(i << 1) + 1] = kHexChars[byte & 0xf];
}
return out_buffer;
}
std::string IntToString(int value) {
// log10(2) ~= 0.3 bytes needed per bit or per byte log10(2**8) ~= 2.4.
// So round up to allocate 3 output characters per byte, plus 1 for '-'.
const int kOutputBufSize = 3 * sizeof(int) + 1;
char buffer[kOutputBufSize];
memset(buffer, 0, kOutputBufSize);
snprintf(buffer, kOutputBufSize, "%d", value);
std::string out_string(buffer, sizeof(buffer));
return out_string;
}
std::string UintToString(unsigned int value) {
// log10(2) ~= 0.3 bytes needed per bit or per byte log10(2**8) ~= 2.4.
// So round up to allocate 3 output characters per byte.
const int kOutputBufSize = 3 * sizeof(unsigned int);
char buffer[kOutputBufSize];
memset(buffer, 0, kOutputBufSize);
snprintf(buffer, kOutputBufSize, "%u", value);
std::string out_string(buffer, sizeof(buffer));
return out_string;
}
}; // namespace wvcdm

View File

@@ -0,0 +1,245 @@
// Copyright 2013 Google Inc. All Rights Reserved.
#include <errno.h>
#include <getopt.h>
#include "cdm_engine.h"
#include "config_test_env.h"
#include "gtest/gtest.h"
#include "license_request.h"
#include "log.h"
#include "string_conversions.h"
#include "url_request.h"
namespace {
// Default license server, can be configured using --server command line option
// Default key id (pssh), can be configured using --keyid command line option
std::string g_client_auth;
wvcdm::KeyId g_key_id;
wvcdm::CdmKeySystem g_key_system;
std::string g_license_server;
std::string g_port;
wvcdm::KeyId g_wrong_key_id;
int g_use_full_path = 0; // cannot use boolean in getopt_long
} // namespace
namespace wvcdm {
class WvCdmEngineTest : public testing::Test {
public:
WvCdmEngineTest() {}
~WvCdmEngineTest() {}
protected:
void GenerateKeyRequest(const std::string& key_system,
const std::string& init_data) {
wvcdm::CdmNameValueMap app_parameters;
EXPECT_EQ(cdm_engine_.GenerateKeyRequest(session_id_,
true, // is_key_system_present
key_system,
init_data,
kLicenseTypeStreaming,
app_parameters,
&key_msg_), wvcdm::KEY_MESSAGE);
}
void GenerateRenewalRequest(const std::string& key_system,
const std::string& init_data) {
EXPECT_EQ(cdm_engine_.GenerateRenewalRequest(session_id_,
true, // is_key_system_init_data_present,
key_system,
init_data,
&key_msg_),
wvcdm::KEY_MESSAGE);
}
// posts a request and extracts the drm message from the response
std::string GetKeyRequestResponse(const std::string& server_url,
const std::string& client_auth,
int expected_response) {
UrlRequest url_request(server_url + client_auth, g_port);
if (!url_request.is_connected()) {
return "";
}
url_request.PostRequest(key_msg_);
std::string response;
int resp_bytes = url_request.GetResponse(response);
LOGD("response:\r\n%s", response.c_str());
LOGD("end %d bytes response dump", resp_bytes);
// Youtube server returns 400 for invalid message while play server returns
// 500, so just test inequity here for invalid message
int status_code = url_request.GetStatusCode(response);
if (expected_response == 200) {
EXPECT_EQ(200, status_code);
} else {
EXPECT_NE(200, status_code);
}
std::string drm_msg;
if (200 == status_code) {
LicenseRequest lic_request;
lic_request.GetDrmMessage(response, drm_msg);
LOGV("drm msg: %u bytes\r\n%s", drm_msg.size(),
HexEncode(reinterpret_cast<const uint8_t*>(drm_msg.data()),
drm_msg.size()).c_str());
}
return drm_msg;
}
void VerifyKeyRequestResponse(const std::string& server_url,
const std::string& client_auth,
std::string& init_data,
bool is_renewal) {
std::string resp = GetKeyRequestResponse(server_url,
client_auth,
200);
if (is_renewal) {
EXPECT_EQ(cdm_engine_.RenewKey(session_id_,
true, // is_key_system_init_data_present
g_key_system,
init_data,
resp), wvcdm::KEY_ADDED);
}
else {
EXPECT_EQ(cdm_engine_.AddKey(session_id_,
true, // is_key_system_init_data_present
g_key_system,
init_data,
resp), wvcdm::KEY_ADDED);
}
}
wvcdm::CdmEngine cdm_engine_;
std::string key_msg_;
std::string session_id_;
};
TEST_F(WvCdmEngineTest, BaseMessageTest) {
cdm_engine_.OpenSession(g_key_system, &session_id_);
GenerateKeyRequest(g_key_system, g_key_id);
GetKeyRequestResponse(g_license_server, g_client_auth, 200);
cdm_engine_.CloseSession(session_id_);
}
TEST_F(WvCdmEngineTest, WrongMessageTest) {
cdm_engine_.OpenSession(g_key_system, &session_id_);
std::string wrong_message = wvcdm::a2bs_hex(g_wrong_key_id);
GenerateKeyRequest(g_key_system, wrong_message);
GetKeyRequestResponse(g_license_server, g_client_auth, 500);
cdm_engine_.CloseSession(session_id_);
}
TEST_F(WvCdmEngineTest, NormalDecryption) {
cdm_engine_.OpenSession(g_key_system, &session_id_);
GenerateKeyRequest(g_key_system, g_key_id);
VerifyKeyRequestResponse(g_license_server, g_client_auth, g_key_id, false);
cdm_engine_.CloseSession(session_id_);
}
TEST_F(WvCdmEngineTest, LicenseRenewal) {
cdm_engine_.OpenSession(g_key_system, &session_id_);
GenerateKeyRequest(g_key_system, g_key_id);
VerifyKeyRequestResponse(g_license_server, g_client_auth, g_key_id, false);
GenerateRenewalRequest(g_key_system, g_key_id);
VerifyKeyRequestResponse(g_license_server, g_client_auth, g_key_id, true);
cdm_engine_.CloseSession(session_id_);
}
} // namespace wvcdm
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
wvcdm::ConfigTestEnv config;
g_client_auth.assign(config.client_auth());
g_key_system.assign(config.key_system());
g_wrong_key_id.assign(config.wrong_key_id());
// The following variables are configurable through command line options.
g_license_server.assign(config.license_server());
g_key_id.assign(config.key_id());
g_port.assign(config.port());
std::string license_server(g_license_server);
int show_usage = 0;
static const struct option long_options[] = {
{ "use_full_path", no_argument, &g_use_full_path, 0 },
{ "keyid", required_argument, NULL, 'k' },
{ "port", required_argument, NULL, 'p' },
{ "server", required_argument, NULL, 's' },
{ NULL, 0, NULL, '\0' }
};
int option_index = 0;
int opt = 0;
while ((opt = getopt_long(argc, argv, "k:p:s:u", long_options, &option_index)) != -1) {
switch (opt) {
case 'k': {
g_key_id.clear();
g_key_id.assign(optarg);
break;
}
case 'p': {
g_port.clear();
g_port.assign(optarg);
break;
}
case 's': {
g_license_server.clear();
g_license_server.assign(optarg);
break;
}
case 'u': {
g_use_full_path = 1;
break;
}
case '?': {
show_usage = 1;
break;
}
}
}
if (show_usage) {
std::cout << std::endl;
std::cout << "usage: " << argv[0] << " [options]" << std::endl << std::endl;
std::cout << " enclose multiple arguments in '' when using adb shell" << std::endl;
std::cout << " e.g. adb shell '" << argv[0] << " --server=\"url\"'" << std::endl << std::endl;
std::cout << std::setw(30) << std::left << " --port=<connection port>";
std::cout << "specifies the port number, in decimal format" << std::endl;
std::cout << std::setw(30) << std::left << " ";
std::cout << "default: " << g_port << std::endl;
std::cout << std::setw(30) << std::left << " --server=<server_url>";
std::cout << "configure the license server url, please include http[s] in the url" << std::endl;
std::cout << std::setw(30) << std::left << " ";
std::cout << "default: " << license_server << std::endl;
std::cout << std::setw(30) << std::left << " --keyid=<key_id>";
std::cout << "configure the key id or pssh, in hex format" << std::endl;
std::cout << std::setw(30) << std::left << " default keyid:";
std::cout << g_key_id << std::endl;
std::cout << std::setw(30) << std::left << " --use_full_path";
std::cout << "specify server url is not a proxy server" << std::endl;
std::cout << std::endl;
return 0;
}
std::cout << std::endl;
std::cout << "Server: " << g_license_server << std::endl;
std::cout << "Port: " << g_port << std::endl;
std::cout << "KeyID: " << g_key_id << std::endl << std::endl;
g_key_id = wvcdm::a2bs_hex(g_key_id);
config.set_license_server(g_license_server);
config.set_port(g_port);
config.set_key_id(g_key_id);
return RUN_ALL_TESTS();
}

View File

@@ -0,0 +1,77 @@
// Copyright 2013 Google Inc. All Rights Reserved.
#include "config_test_env.h"
namespace {
// choice of YT, GP, GP_Huahui, SDK_Hali
#define USE_SERVER_YT 1
#define USE_SERVER_GP 2
#define USE_SERVER_SDK_Hali 3
// select which server to use for testing
#define USE_SERVER USE_SERVER_GP
#if (USE_SERVER == USE_SERVER_SDK_Hali)
static const std::string kLicenseServer =
"http://hamid.kir.corp.google.com:8888/drm";
static const std::string kClientAuth = "";
static const std::string kKeyId =
"000000347073736800000000" // blob size and pssh
"EDEF8BA979D64ACEA3C827DCD51D21ED00000014" // Widevine system id
"0801121030313233343536373839616263646566"; // key - for gHali
#elif (USE_SERVER == USE_SERVER_YT)
static const std::string kLicenseServer =
"https://www.youtube.com/api/drm/widevine?video_id=03681262dc412c06&source=YOUTUBE";
static const std::string kClientAuth = "";
static const std::string kKeyId =
"000000347073736800000000" // blob size and pssh
"EDEF8BA979D64ACEA3C827DCD51D21ED00000014" // Widevine system id
"0801121093789920E8D6520098577DF8F2DD5546"; // pssh data
#elif (USE_SERVER == USE_SERVER_GP)
static const std::string kLicenseServer =
"https://jmt17.google.com/video-dev/license/GetCencLicense";
// NOTE: Append a userdata attribute to place a unique marker that the
// server team can use to track down specific requests during debugging
// e.g., "<existing-client-auth-string>&userdata=<your-ldap>.<your-tag>"
// "<existing-client-auth-string>&userdata=jbmr2.dev"
static const std::string kClientAuth =
"?source=YOUTUBE&video_id=EGHC6OHNbOo&oauth=ya.gtsqawidevine";
static const std::string kKeyId =
"000000347073736800000000" // blob size and pssh
"edef8ba979d64acea3c827dcd51d21ed00000014" // Widevine system id
"08011210e02562e04cd55351b14b3d748d36ed8e"; // pssh data
#else
#error "Must define USE_SERVER"
#endif
//static const char kWidevineKeySystem[] = "com.widevine.alpha";
// An invalid key id, expected to fail
static const std::string kWrongKeyId =
"000000347073736800000000" // blob size and pssh
"EDEF8BA979D64ACEA3C827DCD51D21ED00000014" // Widevine system id
"0901121094889920E8D6520098577DF8F2DD5546"; // pssh data
} // namespace
namespace wvcdm {
ConfigTestEnv::ConfigTestEnv()
: client_auth_(kClientAuth),
key_id_(kKeyId),
key_system_("com.widevine.alpha"),
license_server_(kLicenseServer),
port_("80"),
wrong_key_id_(kWrongKeyId) {
}
} // namespace wvcdm

View File

@@ -0,0 +1,46 @@
// Copyright 2013 Google Inc. All Rights Reserved.
#ifndef CDM_TEST_CONFIG_TEST_ENV_H_
#define CDM_TEST_CONFIG_TEST_ENV_H_
#include <string>
#include "wv_cdm_types.h"
namespace wvcdm {
// Configures default test environment.
class ConfigTestEnv {
public:
ConfigTestEnv();
~ConfigTestEnv() {};
const std::string& client_auth() const { return client_auth_; }
const KeyId& key_id() const { return key_id_; }
const CdmKeySystem& key_system() const { return key_system_; }
const std::string& license_server() const { return license_server_; }
const std::string& port() const { return port_; }
const KeyId& wrong_key_id() const { return wrong_key_id_; }
void set_key_id(KeyId& key_id) { key_id_.assign(key_id); }
void set_key_system(CdmKeySystem& key_system) {
key_system_.assign(key_system);
}
void set_license_server(std::string& license_server) {
license_server_.assign(license_server);
}
void set_port(std::string& port) { port_.assign(port); }
private:
std::string client_auth_;
KeyId key_id_;
CdmKeySystem key_system_;
std::string license_server_;
std::string port_;
KeyId wrong_key_id_;
CORE_DISALLOW_COPY_AND_ASSIGN(ConfigTestEnv);
};
}; // namespace wvcdm
#endif // CDM_TEST_CONFIG_TEST_ENV_H_

View File

@@ -0,0 +1,192 @@
// Copyright 2013 Google Inc. All Rights Reserved.
#include "http_socket.h"
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <string.h>
#include <sys/socket.h>
#include "log.h"
namespace wvcdm {
HttpSocket::HttpSocket() : socket_fd_(-1), timeout_enabled_(false) {}
HttpSocket::~HttpSocket()
{
CloseSocket();
}
void HttpSocket::CloseSocket()
{
if (socket_fd_ != -1) {
close(socket_fd_);
socket_fd_ = -1;
}
}
// Extracts the domain name and resource path from the input url parameter.
// The results are put in domain_name and resource_path respectively.
// The format of the url can begin with <protocol/scheme>:://domain server/...
// or dowmain server/resource_path
void HttpSocket::GetDomainNameAndPathFromUrl(const std::string& url,
std::string& domain_name,
std::string& resource_path) {
domain_name.clear();
resource_path.clear();
size_t start = url.find("//");
size_t end = url.npos;
if (start != url.npos) {
end = url.find("/", start + 2);
if (end != url.npos) {
domain_name.assign(url, start + 2, end - start - 2);
resource_path.assign(url, end + 1, url.npos);
} else {
domain_name.assign(url, start + 2, url.npos);
}
} else {
// no scheme/protocol in url
end = url.find("/");
if (end != url.npos) {
domain_name.assign(url, 0, end);
resource_path.assign(url, end + 1, url.npos);
} else {
domain_name.assign(url);
}
}
// strips port number if present, e.g. https://www.domain.com:8888/...
end = domain_name.find(":");
if (end != domain_name.npos) {
domain_name.erase(end);
}
}
bool HttpSocket::Connect(const char* url, const std::string& port, bool enable_timeout)
{
GetDomainNameAndPathFromUrl(url, domain_name_, resource_path_);
socket_fd_ = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd_ < 0) {
LOGE("cannot open socket %d", errno);
return false;
}
int reuse = 1;
if (setsockopt(socket_fd_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1) {
CloseSocket();
LOGE("setsockopt error %d", errno);
return false;
}
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
struct addrinfo* addr_info = NULL;
bool status = true;
int ret = getaddrinfo(domain_name_.c_str(), port.c_str(), &hints, &addr_info);
if (ret != 0) {
CloseSocket();
LOGE("getaddrinfo failed with %d", ret);
status = false;
} else {
if (connect(socket_fd_, addr_info->ai_addr, addr_info->ai_addrlen) == -1) {
CloseSocket();
LOGE("cannot connect socket to %s, error=%d", domain_name_.c_str(), errno);
status = false;
}
}
timeout_enabled_ = enable_timeout;
if (addr_info != NULL) {
freeaddrinfo(addr_info);
}
return status;
}
int HttpSocket::Read(char* data, int len) {
return(Read(data, len, 0));
}
// makes non-blocking mode only during read, it supports timeout for read
// returns -1 for error, number of bytes read for success
int HttpSocket::Read(char* data, int len, int timeout_in_ms)
{
bool use_timeout = (timeout_enabled_ && (timeout_in_ms > 0));
int original_flags = 0;
if (use_timeout) {
original_flags = fcntl(socket_fd_, F_GETFL, 0);
if (original_flags == -1) {
LOGE("fcntl error %d", errno);
return -1;
}
if (fcntl(socket_fd_, F_SETFL, original_flags | O_NONBLOCK) == -1) {
LOGE("fcntl error %d", errno);
return -1;
}
}
int total_read = 0;
int read = 0;
int to_read = len;
while (to_read > 0) {
if (use_timeout) {
fd_set read_fds;
struct timeval tv;
tv.tv_sec = timeout_in_ms / 1000;
tv.tv_usec = (timeout_in_ms % 1000) * 1000;
FD_ZERO(&read_fds);
FD_SET(socket_fd_, &read_fds);
if (select(socket_fd_ + 1, &read_fds, NULL, NULL, &tv) == -1) {
LOGE("select failed");
break;
}
if (!FD_ISSET(socket_fd_, &read_fds)) {
LOGE("socket read timeout");
break;
}
}
read = recv(socket_fd_, data, to_read, 0);
if (read > 0) {
to_read -= read;
data += read;
total_read += read;
} else if (read == 0) {
// in blocking mode, zero read mean's peer closed.
// in non-blocking mode, select said that there is data. so it should not happen
break;
} else {
LOGE("recv returned %d, error = %d", read, errno);
break;
}
}
if (use_timeout) {
fcntl(socket_fd_, F_SETFL, original_flags); // now blocking again
}
return total_read;
}
int HttpSocket::Write(const char* data, int len)
{
int total_sent = 0;
int sent = 0;
int to_send = len;
while (to_send > 0) {
sent = send(socket_fd_, data, to_send, 0);
if (sent > 0) {
to_send -= sent;
data += sent;
total_sent += sent;
} else if (sent == 0) {
usleep(10); // retry later
} else {
LOGE("send returned error %d", errno);
}
}
return total_sent;
}
} // namespace wvcdm

View File

@@ -0,0 +1,39 @@
// Copyright 2013 Google Inc. All Rights Reserved.
#ifndef CDM_TEST_HTTP_SOCKET_H_
#define CDM_TEST_HTTP_SOCKET_H_
#include <string>
#include "wv_cdm_types.h"
namespace wvcdm {
// Provides basic Linux based TCP socket interface.
class HttpSocket {
public:
HttpSocket();
~HttpSocket();
void CloseSocket();
bool Connect(const char* url, const std::string& port, bool enable_timeout);
void GetDomainNameAndPathFromUrl(const std::string& url,
std::string& domain_name,
std::string& resource_path);
const std::string& domain_name() const { return domain_name_; };
const std::string& resource_path() const { return resource_path_; };
int Read(char* data, int len);
int Read(char* data, int len, int timeout_in_ms);
int Write(const char* data, int len);
private:
std::string domain_name_;
std::string resource_path_;
int socket_fd_;
bool timeout_enabled_;
CORE_DISALLOW_COPY_AND_ASSIGN(HttpSocket);
};
}; // namespace wvcdm
#endif // CDM_TEST_HTTP_SOCKET_H_

View File

@@ -0,0 +1,194 @@
// Copyright 2013 Google Inc. All Rights Reserved.
#include <errno.h>
#include "gtest/gtest.h"
#include "http_socket.h"
#include "log.h"
#include "string_conversions.h"
namespace {
std::string gTestServer("https://www.google.com");
std::string gTestData("Hello");
const int kHttpBufferSize = 4096;
char gBuffer[kHttpBufferSize];
}
namespace wvcdm {
class HttpSocketTest : public testing::Test {
public:
HttpSocketTest() {}
~HttpSocketTest() { socket_.CloseSocket(); }
protected:
bool Connect(const std::string& server_url) {
if (socket_.Connect(server_url.c_str(), "80", true)) {
LOGD("connected to %s", socket_.domain_name().c_str());
} else {
LOGE("failed to connect to %s", socket_.domain_name().c_str());
return false;
}
return true;
}
bool PostRequest(const std::string& data) {
std::string request("POST ");
if (socket_.resource_path().empty())
request.append(socket_.domain_name());
else
request.append(socket_.resource_path());
request.append(" HTTP/1.1\r\n");
request.append("Host: ");
request.append(socket_.domain_name());
request.append("\r\nUser-Agent: httpSocketTest/1.0\r\n");
request.append("Content-Length: ");
memset(gBuffer, 0, kHttpBufferSize);
snprintf(gBuffer, kHttpBufferSize, "%d\r\n", static_cast<int>(data.size()));
request.append(gBuffer);
request.append("Content-Type: multipart/form-data\r\n");
// newline terminates header
request.append("\r\n");
// append data
request.append(data);
socket_.Write(request.c_str(), request.size());
LOGD("request: %s", request.c_str());
return true;
}
bool GetResponse() {
int bytes = socket_.Read(gBuffer, kHttpBufferSize, 1000);
if (bytes < 0) {
LOGE("read error = ", errno);
return false;
} else {
LOGD("read %d bytes", bytes);
std::string response(gBuffer, bytes);
LOGD("response: %s", response.c_str());
LOGD("end response dump");
return true;
}
}
HttpSocket socket_;
std::string domain_name_;
std::string resource_path_;
};
TEST_F(HttpSocketTest, GetDomainNameAndPathFromUrlTest)
{
socket_.GetDomainNameAndPathFromUrl("http://code.google.com/p/googletest/wiki/Primer",
domain_name_,
resource_path_);
EXPECT_STREQ("code.google.com", domain_name_.c_str());
EXPECT_STREQ("p/googletest/wiki/Primer", resource_path_.c_str());
socket_.GetDomainNameAndPathFromUrl("http://code.google.com/p/googletest/wiki/Primer/",
domain_name_,
resource_path_);
EXPECT_STREQ("code.google.com", domain_name_.c_str());
EXPECT_STREQ("p/googletest/wiki/Primer/", resource_path_.c_str());
socket_.GetDomainNameAndPathFromUrl("http://code.google.com/",
domain_name_,
resource_path_);
EXPECT_STREQ("code.google.com", domain_name_.c_str());
EXPECT_STREQ("", resource_path_.c_str());
socket_.GetDomainNameAndPathFromUrl("http://code.google.com",
domain_name_,
resource_path_);
EXPECT_STREQ("code.google.com", domain_name_.c_str());
EXPECT_STREQ("", resource_path_.c_str());
socket_.GetDomainNameAndPathFromUrl("code.google.com/p/googletest/wiki/Primer",
domain_name_,
resource_path_);
EXPECT_STREQ("code.google.com", domain_name_.c_str());
EXPECT_STREQ("p/googletest/wiki/Primer", resource_path_.c_str());
socket_.GetDomainNameAndPathFromUrl("code.google.com",
domain_name_,
resource_path_);
EXPECT_STREQ("code.google.com", domain_name_.c_str());
EXPECT_STREQ("", resource_path_.c_str());
socket_.GetDomainNameAndPathFromUrl("code.google.com/",
domain_name_,
resource_path_);
EXPECT_STREQ("code.google.com", domain_name_.c_str());
EXPECT_STREQ("", resource_path_.c_str());
socket_.GetDomainNameAndPathFromUrl("",
domain_name_,
resource_path_);
EXPECT_TRUE(domain_name_.empty());
EXPECT_TRUE(resource_path_.empty());
socket_.GetDomainNameAndPathFromUrl("http://10.21.200.68:8888/drm",
domain_name_,
resource_path_);
EXPECT_STREQ("10.21.200.68", domain_name_.c_str());
EXPECT_STREQ("drm", resource_path_.c_str());
socket_.GetDomainNameAndPathFromUrl("http://10.21.200.68:8888",
domain_name_,
resource_path_);
EXPECT_STREQ("10.21.200.68", domain_name_.c_str());
EXPECT_TRUE(resource_path_.empty());
}
TEST_F(HttpSocketTest, ConnectTest)
{
EXPECT_TRUE(Connect(gTestServer));
socket_.CloseSocket();
EXPECT_FALSE(Connect("ww.g.c"));
socket_.CloseSocket();
}
TEST_F(HttpSocketTest, RoundTripTest)
{
ASSERT_TRUE(Connect(gTestServer));
EXPECT_TRUE(PostRequest(gTestData));
GetResponse();
socket_.CloseSocket();
}
} // namespace wvcdm
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
std::string temp;
std::string test_server(gTestServer);
std::string test_data(gTestData);
for (int i=1; i<argc; i++) {
temp.assign(argv[i]);
if (temp.find("--server=") == 0) {
gTestServer.assign(temp.substr(strlen("--server=")));
} else if (temp.find("--data=") == 0) {
gTestData.assign(temp.substr(strlen("--data=")));
}
else {
std::cout << "error: unknown option '" << argv[i] << "'" << std::endl;
std::cout << "usage: http_socket_test [options]" << std::endl << std::endl;
std::cout << std::setw(30) << std::left << " --server=<server_url>";
std::cout << "configure the test server url, please include http[s] in the url" << std::endl;
std::cout << std::setw(30) << std::left << " ";
std::cout << "default: " << test_server << std::endl;
std::cout << std::setw(30) << std::left << " --data=<data>";
std::cout << "configure data to send, in ascii string format" << std::endl;
std::cout << std::setw(30) << std::left << " ";
std::cout << "default: " << test_data << std::endl << std::endl;
return 0;
}
}
std::cout << std::endl;
std::cout << "Server: " << gTestServer << std::endl;
std::cout << "Data: " << gTestData << std::endl;
return RUN_ALL_TESTS();
}

View File

@@ -0,0 +1,82 @@
// Copyright 2013 Google Inc. All Rights Reserved.
#include "license_request.h"
#include "log.h"
namespace wvcdm {
static const std::string kTwoBlankLines("\r\n\r\n");
size_t LicenseRequest::FindHeaderEndPosition(
const std::string& response) const {
return(response.find(kTwoBlankLines));
}
// Returns drm message in drm_msg.
// The drm message is at the end of the response message.
void LicenseRequest::GetDrmMessage(const std::string& response,
std::string& drm_msg) {
if (response.empty()) {
drm_msg.clear();
return;
}
// Extracts DRM message.
// Content-Length = GLS line + Header(s) + empty line + drm message;
// we use the empty line to locate the drm message, and compute
// the drm message length as below instead of using Content-Length
size_t header_end_pos = FindHeaderEndPosition(response);
if (header_end_pos != std::string::npos) {
header_end_pos += kTwoBlankLines.size(); // points to response body
drm_msg.clear();
size_t drm_msg_pos = response.find(kTwoBlankLines, header_end_pos);
if (drm_msg_pos != std::string::npos) {
drm_msg_pos += kTwoBlankLines.size(); // points to drm message
} else {
// For backward compatibility, no blank line after error code
drm_msg_pos = response.find("\r\n", header_end_pos);
if (drm_msg_pos != std::string::npos) {
drm_msg_pos += 2; // points to drm message
}
}
if (drm_msg_pos != std::string::npos) {
drm_msg = response.substr(drm_msg_pos);
} else {
LOGE("drm msg not found");
}
} else {
LOGE("response body not found");
}
}
// Returns heartbeat url in heartbeat_url.
// The heartbeat url is stored as meta data in the response message.
void LicenseRequest::GetHeartbeatUrl(const std::string& response,
std::string& heartbeat_url) {
if (response.empty()) {
heartbeat_url.clear(); // TODO: assign default heartbeat url
return;
}
size_t header_end_pos = FindHeaderEndPosition(response);
if (header_end_pos != std::string::npos) {
header_end_pos += kTwoBlankLines.size(); // points to response body
heartbeat_url.clear();
size_t heartbeat_url_pos = response.find("Heartbeat-Url: ",
header_end_pos);
if (heartbeat_url_pos != std::string::npos) {
heartbeat_url_pos += sizeof("Heartbeat-Url: ");
heartbeat_url.assign(response.substr(heartbeat_url_pos));
} else {
LOGE("heartbeat url not found");
}
} else {
LOGE("response body not found");
}
}
} // namespace wvcdm

View File

@@ -0,0 +1,30 @@
// Copyright 2013 Google Inc. All Rights Reserved.
#ifndef CDM_TEST_LICENSE_REQUEST_H_
#define CDM_TEST_LICENSE_REQUEST_H_
#include <string>
#include "wv_cdm_types.h"
namespace wvcdm {
// Parses response from a license request.
// This class assumes a particular response format defined by
// Google license servers.
class LicenseRequest {
public:
LicenseRequest() {};
~LicenseRequest() {};
void GetDrmMessage(const std::string& response, std::string& drm_msg);
void GetHeartbeatUrl(const std::string& response, std::string& heartbeat_url);
private:
size_t FindHeaderEndPosition(const std::string& response) const;
CORE_DISALLOW_COPY_AND_ASSIGN(LicenseRequest);
};
}; // namespace wvcdm
#endif // CDM_TEST_LICENSE_REQUEST_H_

View File

@@ -0,0 +1,100 @@
// Copyright 2012 Google Inc. All Rights Reserved.
#include "crypto_engine.h"
#include "crypto_session.h"
#include "license.h"
#include "gtest/gtest.h"
#include "string_conversions.h"
namespace {
// The test data is based on key box Eureka-Dev-G1-0001520
// This unit test should run on oemcrypto mock with the same key box
static const char* kInitData = "0801121093789920E8D6520098577DF8F2DD5546";
static const char* kSignedRequest =
"080112790A4C0800124800000002000001241F344DB9DFF087F01D917910F39B"
"60DC7797CD97789EE82516DC07A478CB70B8C08C299293150AA8E01D2DC9808C"
"98DAA16E40A0E55DFE3618C7584DD3C7BE4212250A230A140801121093789920"
"E8D6520098577DF8F2DD554610011A09393837363534333231180120001A20FA"
"E2DDCD7F1ACA4B728EC957FEE802F8A5541557ACA784EE0D05BFCC0E65FEA1";
static const char* kValidResponse =
"080212D9020A190A093938373635343332311208C434AB9240A9EF2420012800"
"120E0801180120809A9E0128809A9E011A461210B72EEBF582B04BDB15C2E0E3"
"20B21C351A30E51FC1D27F70DB8E0DDF8C051BD6E251A44599DBCE4E1BE663FD"
"3AFAB191A7DD5736841FB04CE558E7F17BD9812A2DBA20011A6E0A1093789920"
"E8D6520098577DF8F2DD55461210367E8714B6F10087AFDE542EDC5C91541A20"
"ED51D4E84D81C8CBD8E2046EE079F8A2016268A2F192B902FDA241FEEB10C014"
"200242240A109209D46191B8752147C9F6A1CE2BEE6E12107910F39B60DC7797"
"CD97789EE82516DC1A6E0A107B1328EB61B554E293F75B1E3E94CC3B1210676F"
"69BBDA35EE972B77BC1328A087391A20D2B9FA92B164F5F6362CAD9200A11661"
"B8F71E9CE671A3A252D34586526B68FA200242240A109D7B13420FD6217666CC"
"CD43860FAA3A1210DBCE4E1BE663FD3AFAB191A7DD57368420E9FDCE86051A20"
"C6279E32FD2CB9067229E87AFF4B2DE14A077CDF8F061DAEE2CC2D1BCDEF62D0";
static const char* kInvalidResponse =
"0802128D020A190A093938373635343332311208BA68C949396C438C20012800"
"120E0801180120809A9E0128809A9E011A4612105021EB9AEDC1F73E96DE7DCC"
"6D7D72401A300A82E118C0BF0DB230FCADE3F49A9777DDD392322240FEF32C97"
"F85428E2F6CCFA638B5481464ADBCF199CEC2FCF3AFB20011A480A1093789920"
"E8D6520098577DF8F2DD55461210EE52C59B99050A36E10569AFB34D1DA41A20"
"C61FCB8019AC9ADE99FF8FCA99ED35E2331B6488A35102F9379AA42C87A22DC7"
"20021A480A107B1328EB61B554E293F75B1E3E94CC3B12101BBF5286B859E349"
"2E4A47A24C06AC1B1A2061F21836A04E558BEE0244EF41C165F60CF23C580275"
"3175D48BAF1C6CA5759F200220A2BCCA86051A203FD4671075D9DEC6486A9317"
"70669993306831EDD57D77F34EFEB467470BA364";
}
namespace wvcdm {
class LicenseTest : public ::testing::Test {
protected:
virtual void SetUp() {
CryptoEngine* crypto_engine = CryptoEngine::GetInstance();
EXPECT_TRUE(crypto_engine != NULL);
session_ = crypto_engine->CreateSession("Dummy");
EXPECT_TRUE(session_ != NULL);
std::string token;
EXPECT_TRUE(crypto_engine->GetToken(&token));
EXPECT_TRUE(session_->IsOpen());
EXPECT_TRUE(license_.Init(token, session_));
}
virtual void TearDown() {
session_->Close();
delete session_;
}
CryptoSession* session_;
CdmLicense license_;
};
TEST(LicenseTestSession, InitNullSession) {
CdmLicense license;
EXPECT_FALSE(license.Init("Dummy", NULL));
}
TEST_F(LicenseTest, PrepareKeyRequest) {
std::string signed_request;
license_.PrepareKeyRequest(a2bs_hex(kInitData), &signed_request);
EXPECT_EQ(signed_request, a2bs_hex(kSignedRequest));
}
TEST_F(LicenseTest, HandleKeyResponseValid) {
std::string signed_request;
license_.PrepareKeyRequest(a2bs_hex(kInitData), &signed_request);
EXPECT_EQ(signed_request, a2bs_hex(kSignedRequest));
EXPECT_TRUE(license_.HandleKeyResponse(a2bs_hex(kValidResponse)));
}
TEST_F(LicenseTest, HandleKeyResponseInvalid) {
std::string signed_request;
license_.PrepareKeyRequest(a2bs_hex(kInitData), &signed_request);
EXPECT_EQ(signed_request, a2bs_hex(kSignedRequest));
EXPECT_FALSE(license_.HandleKeyResponse(a2bs_hex(kInvalidResponse)));
}
// TODO(kqyang): add unit test cases for PrepareKeyRenewalRequest
// and HandleRenewalKeyResponse
}

View File

@@ -0,0 +1,106 @@
// Copyright 2013 Google Inc. All Rights Reserved.
#include "url_request.h"
#include "http_socket.h"
#include "log.h"
namespace wvcdm {
UrlRequest::UrlRequest(const std::string& url, const std::string& port)
: is_connected_(false),
port_("80"),
request_(""),
server_url_(url)
{
if (!port.empty()) {
port_.assign(port);
}
if (socket_.Connect((server_url_).c_str(), port_, true)) {
LOGD("connected to %s", socket_.domain_name().c_str());
is_connected_ = true;
} else {
LOGE("failed to connect to %s, port=%s",
socket_.domain_name().c_str(), port.c_str());
}
}
UrlRequest::~UrlRequest()
{
socket_.CloseSocket();
}
void UrlRequest::AppendChunkToUpload(const std::string& data) {
// format of chunk:
// size of chunk in hex\r\n
// data\r\n
// . . .
// 0\r\n
// buffer to store length of chunk
memset(buffer_, 0, kHttpBufferSize);
snprintf(buffer_, kHttpBufferSize, "%x\r\n", data.size());
request_.append(buffer_); // appends size of chunk
LOGD("...\r\n%s", request_.c_str());
request_.append(data);
request_.append("\r\n"); // marks end of data
}
int UrlRequest::GetResponse(std::string& response) {
response.clear();
const int kTimeoutInMs = 1000;
int bytes = 0;
int total_bytes = 0;
do {
memset(buffer_, 0, kHttpBufferSize);
bytes = socket_.Read(buffer_, kHttpBufferSize, kTimeoutInMs);
if (bytes > 0) {
response.append(buffer_, bytes);
total_bytes += bytes;
} else {
if (bytes < 0) LOGE("read error = ", errno);
// bytes == 0 indicates nothing to read
}
} while (bytes > 0);
return total_bytes;
}
int UrlRequest::GetStatusCode(const std::string& response) {
const std::string kHttpVersion("HTTP/1.1");
int status_code = -1;
size_t pos = response.find(kHttpVersion);
if (pos != std::string::npos) {
pos += kHttpVersion.size();
sscanf(response.substr(pos).c_str(), "%d", &status_code);
}
return status_code;
}
bool UrlRequest::PostRequest(const std::string& data) {
request_.assign("POST /");
request_.append(socket_.resource_path());
request_.append(" HTTP/1.1\r\n");
request_.append("Host: ");
request_.append(socket_.domain_name());
request_.append("\r\nConnection: Keep-Alive\r\n");
request_.append("Transfer-Encoding: chunked\r\n");
request_.append("User-Agent: Widevine CDM v1.0\r\n");
request_.append("Accept-Encoding: gzip,deflate\r\n");
request_.append("Accept-Language: en-us,fr\r\n");
request_.append("Accept-Charset: iso-8859-1,*,utf-8\r\n");
request_.append("\r\n"); // empty line to terminate header
// calls AppendChunkToUpload repeatedly for multiple chunks
AppendChunkToUpload(data);
// terminates last chunk with 0\r\n, then ends header with an empty line
request_.append("0\r\n\r\n");
socket_.Write(request_.c_str(), request_.size());
return true;
}
} // namespace wvcdm

View File

@@ -0,0 +1,39 @@
// Copyright 2013 Google Inc. All Rights Reserved.
#ifndef CDM_TEST_URL_REQUEST_H_
#define CDM_TEST_URL_REQUEST_H_
#include <string>
#include "http_socket.h"
#include "wv_cdm_types.h"
namespace wvcdm {
// Provides simple HTTP request and response service.
// Only POST request method is implemented.
class UrlRequest {
public:
UrlRequest(const std::string& url, const std::string& port);
~UrlRequest();
void AppendChunkToUpload(const std::string& data);
int GetResponse(std::string& response);
int GetStatusCode(const std::string& response);
bool is_connected() const { return is_connected_; }
bool PostRequest(const std::string& data);
private:
static const unsigned int kHttpBufferSize = 4096;
char buffer_[kHttpBufferSize];
bool is_connected_;
std::string port_;
std::string request_;
HttpSocket socket_;
std::string server_url_;
CORE_DISALLOW_COPY_AND_ASSIGN(UrlRequest);
};
}; // namespace wvcdm
#endif // CDM_TEST_URL_REQUEST_H_

View File

@@ -0,0 +1,861 @@
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <string>
#include <vector>
#include <map>
#include <sstream>
#include "base/at_exit.h"
#include "base/basictypes.h"
#include "base/bind.h"
#include "base/message_loop.h"
#include "base/sys_byteorder.h"
#include "crypto/encryptor.h"
#include "crypto/hmac.h"
#include "crypto/symmetric_key.h"
#include "license_protocol.pb.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "eureka/widevine_cdm/oemcrypto/client/oemcrypto_client.h"
#include "eureka/widevine_cdm/oemcrypto/mock/src/cmac.h"
#include "eureka/widevine_cdm/oemcrypto/mock/src/oemcrypto_keybox_mock.h"
#include "wv_cdm_types.h"
#include "wv_content_decryption_module.h"
namespace {
namespace wv_license_protocol = video_widevine_server::sdk;
using wv_license_protocol::License;
using wv_license_protocol::LicenseIdentification;
using wv_license_protocol::LicenseRequest;
using wv_license_protocol::SessionState;
using wv_license_protocol::SignedMessage;
enum PolicyType {
kDefault = 0,
kNoPlay,
kShortDuration
};
struct PolicyItem {
PolicyType type;
bool can_play;
bool can_renew;
int duration_seconds;
int renewal_delay_seconds;
int renewal_retry_interval_seconds;
};
struct PolicyItem PolicyItems[] = {
{
kDefault,
true,
true,
1000,
100,
0
},
{
kShortDuration,
true,
true,
12,
2,
2
},
{
kNoPlay,
false,
false,
0,
0,
0
}
};
// TODO(jfore): Move this into the test class.
/*const*/ char kTestSigningKey[] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
};
const int kTestSigningKeySize = arraysize(kTestSigningKey);
const char* kEncryptionKeyLabel = "ENCRYPTION";
const uint32_t kEncryptionKeySizeBits = 128;
const char* kSigningKeyLabel = "AUTHENTICATION";
const uint32_t kSigningKeySizeBits = 256;
// This is a container to hold the info for an encrypted frame.
struct WvCdmEncryptedFrameInfo {
char plain_text[32];
int plain_text_size;
uint8_t key_id[32];
int key_id_size;
uint8_t content_key[32];
int content_key_size;
uint8_t encrypted_data[64];
int encrypted_data_size;
PolicyType policy_type;
};
const WvCdmEncryptedFrameInfo kWvCdmEncryptedFrames[] = {
{
"Original data.", 14,
{ 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35
}, 16,
{ 0xeb, 0xdd, 0x62, 0xf1, 0x68, 0x14, 0xd2, 0x7b,
0x68, 0xef, 0x12, 0x2a, 0xfc, 0xe4, 0xae, 0x3c
}, 16,
{ 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x82, 0x54, 0xab, 0x89, 0xa2, 0x75, 0xcd,
0x85, 0x83, 0x61, 0x3f, 0xfe, 0x13, 0x58
}, 23,
kShortDuration
},
{
"Original data.", 14,
{ 0x08, 0x01, 0x12, 0x10, 0x6f, 0x13, 0x33, 0xe7,
0x6e, 0x59, 0x5e, 0xb5, 0x8c, 0x04, 0x30, 0x72,
0xcb, 0xb2, 0x50, 0x68
}, 20,
{ 0xeb, 0xdd, 0x62, 0xf1, 0x68, 0x14, 0xd2, 0x7b,
0x68, 0xef, 0x12, 0x2a, 0xfc, 0xe4, 0xae, 0x3c
}, 16,
{ 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x82, 0x54, 0xab, 0x89, 0xa2, 0x75, 0xcd,
0x85, 0x83, 0x61, 0x3f, 0xfe, 0x13, 0x58
}, 23,
kShortDuration
},
{
"Changed Original data.", 22,
{ 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02,
0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02
}, 16,
{ 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23
}, 16,
{ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x57, 0x66, 0xf4, 0x12, 0x1a, 0xed, 0xb5,
0x79, 0x1c, 0x8e, 0x25, 0xd7, 0x17, 0xe7, 0x5e,
0x16, 0xe3, 0x40, 0x08, 0x27, 0x11, 0xe9
}, 31,
kShortDuration
},
{
"Original data.", 14,
{ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f
}, 16,
{ 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40
}, 16,
{ 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x9c, 0x71, 0x26, 0x57, 0x3e, 0x25, 0x37,
0xf7, 0x31, 0x81, 0x19, 0x64, 0xce, 0xbc
}, 23,
kShortDuration
},
// For license renewal test. This has kNoPlay.
{
// Differnent key and key id.
"Original data.", 14,
{ 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b,
0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33
}, 16,
{ 0xeb, 0xdd, 0x62, 0xf1, 0x68, 0x14, 0xd2, 0x7b,
0x68, 0xef, 0x12, 0x2a, 0xfc, 0xe4, 0xae, 0x3c
}, 16,
{ 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x82, 0x54, 0xab, 0x89, 0xa2, 0x75, 0xcd,
0x85, 0x83, 0x61, 0x3f, 0xfe, 0x13, 0x58
}, 23,
kNoPlay
}
};
bool GetPolicy(PolicyType policy_type, License::Policy* policy) {
DCHECK(policy);
PolicyItem policy_item;
switch (policy_type) {
case kDefault:
policy_item = PolicyItems[0];
DCHECK_EQ(policy_item.type, kDefault);
break;
case kShortDuration:
policy_item = PolicyItems[1];
DCHECK_EQ(policy_item.type, kShortDuration);
break;
case kNoPlay:
policy_item = PolicyItems[2];
DCHECK_EQ(policy_item.type, kNoPlay);
break;
default:
NOTREACHED();
return false;
}
policy->set_can_play(policy_item.can_play);
policy->set_can_renew(policy_item.can_renew);
policy->set_license_duration_seconds(policy_item.duration_seconds);
policy->set_renewal_delay_seconds(policy_item.renewal_delay_seconds);
policy->set_renewal_retry_interval_seconds(
policy_item.renewal_retry_interval_seconds);
return true;
}
SessionState GetTestSessionState() {
static const std::string kTestSessionId = "SomeSessionId";
SessionState session_cache;
session_cache.mutable_license_id()->set_session_id(kTestSessionId);
session_cache.mutable_license_id()->set_version(0);
session_cache.set_signing_key(kTestSigningKey, kTestSigningKeySize);
return session_cache;
}
// Since "GetTime" is used in this test where some functions cannot reach
// Host instance, this will be used as a universal clock for this test.
double GetCurrentTestTime() {
return base::Time::Now().ToDoubleT();
}
// This encrypts the content key using the device key in cdm/base/device_key.h.
std::string EncryptUsingKey(const std::string& input,
const std::string& iv,
const std::string& encryption_key) {
//static const int kAesBlockSize = 16;
crypto::Encryptor aes_ecryptor;
scoped_ptr<crypto::SymmetricKey> device_key(
crypto::SymmetricKey::Import(crypto::SymmetricKey::AES,
encryption_key));
if (!aes_ecryptor.Init(device_key.get(), crypto::Encryptor::CBC, iv))
return "";
std::string encrypted_data;
if (!aes_ecryptor.Encrypt(input, &encrypted_data))
return "";
return encrypted_data;
}
// Takes the license and the session state and generates a SignedMessage. The
// return value is the serialized version of the SignedMessage object.
std::string GenerateSignedLicenseResponse(const License& license,
const std::string& signing_key) {
SignedMessage signed_message;
bool success = license.SerializeToString(
signed_message.mutable_msg());
DCHECK(success);
crypto::HMAC hmacer(crypto::HMAC::SHA256);
if (!hmacer.Init(signing_key))
return "";
static const int kDigestSize = 32;
uint8_t digest[kDigestSize] = { 0 };
if (!hmacer.Sign(signed_message.msg(), digest, kDigestSize))
return "";
signed_message.set_signature(digest, kDigestSize);
std::string signed_message_bytes;
success = signed_message.SerializeToString(&signed_message_bytes);
DCHECK(success);
return signed_message_bytes;
}
// Note: We only use one session. So there aren't any list of sessions stored
// anywhere.
std::string GenerateNewSignedLicense(
const LicenseRequest& license_request,
const License::Policy& policies,
const License::KeyContainer& content_key,
const std::string& encryption_key,
const std::string& signing_key) {
DCHECK(license_request.content_id().has_cenc_id());
SessionState session_cache = GetTestSessionState();
DCHECK(session_cache.has_signing_key());
session_cache.mutable_license_id()->set_request_id(
license_request.content_id().webm_id().request_id());
session_cache.mutable_license_id()->set_type(
license_request.content_id().webm_id().license_type());
License license;
license.mutable_id()->CopyFrom(session_cache.license_id());
license.mutable_policy()->CopyFrom(policies);
license.set_license_start_time(GetCurrentTestTime());
License::KeyContainer* renewal_signing_key = license.add_key();
renewal_signing_key->set_key(session_cache.signing_key());
renewal_signing_key->set_type(License::KeyContainer::SIGNING);
license.add_key()->CopyFrom(content_key);
for (int i = 0; i < license.key_size(); ++i) {
license.mutable_key(i)->set_iv("0123456789012345");
license.mutable_key(i)->set_key(
EncryptUsingKey(license.key(i).key(),
license.key(i).iv(),
encryption_key));
if (license.key(i).key().empty())
return "";
}
return GenerateSignedLicenseResponse(license, signing_key);
}
bool GetContentKeyFromKeyId(const std::string& key_id,
std::string* content_key) {
DCHECK(content_key);
for (unsigned int i = 0; i < arraysize(kWvCdmEncryptedFrames); ++i) {
const WvCdmEncryptedFrameInfo& frame = kWvCdmEncryptedFrames[i];
if (frame.key_id_size != static_cast<int>(key_id.size()))
continue;
if (!memcmp(frame.key_id, key_id.data(), frame.key_id_size)) {
content_key->assign(frame.content_key,
frame.content_key + frame.content_key_size);
return true;
}
}
return false;
}
std::string DeriveKey(const std::string& key,
const std::string& purpose,
const std::string& context,
const uint32_t size_bits) {
if (key.size() != 16)
return "";
// We only handle even multiples of 16 bytes (128 bits) right now.
if ((size_bits % 128) || (size_bits > (128 * 255))) {
return "";
}
std::string result;
const EVP_CIPHER *cipher = EVP_aes_128_cbc();
CMAC_CTX* cmac_ctx = CMAC_CTX_new();
size_t reslen;
unsigned char res[128];
unsigned char counter;
for (counter = 1; counter <= (size_bits / 128); counter++) {
if (!CMAC_Init(cmac_ctx, key.data(), key.size(), cipher, 0))
break;
std::string message;
message.append(1, counter);
message.append(purpose);
message.append(1, '\0');
message.append(context);
uint32_t size_l = htonl(size_bits);
message.append(reinterpret_cast<char*>(&size_l), sizeof(size_l));
if (!CMAC_Update(cmac_ctx, message.data(), message.size()))
break;
if (!CMAC_Final(cmac_ctx, res, &reslen))
break;
result.append((const char*)res, reslen);
}
CMAC_CTX_free(cmac_ctx);
if (counter <= (size_bits / 128))
return "";
return result;
}
bool VerifyTestSignature(const std::string& message,
const std::string& signature,
const std::string& key) {
crypto::HMAC hmacer(crypto::HMAC::SHA256);
if (!hmacer.Init(key))
return false;
if (!hmacer.Verify(message, signature))
return false;
return true;
}
std::string GenerateNewLicenseResponse(const LicenseRequest& license_request,
const PolicyType& policy_type) {
if (!license_request.content_id().has_cenc_id())
return "";
std::string content_key_id = license_request.content_id().cenc_id().pssh(0);
std::string content_key;
if (!GetContentKeyFromKeyId(content_key_id,
&content_key)) {
return "";
}
if (content_key_id.size() > 16)
content_key_id.resize(16);
License::Policy policies;
if (!GetPolicy(policy_type, &policies))
return "";
std::string context;
if (!license_request.SerializeToString(&context))
return "";
wvoec_mock::WvKeybox keybox;
// TODO(): Fix this to use a constant for key length.
std::string widevine_device_key(keybox.device_key().value());
std::string encryption_key = DeriveKey(widevine_device_key,
std::string(kEncryptionKeyLabel),
context,
kEncryptionKeySizeBits);
std::string signing_key = DeriveKey(widevine_device_key,
std::string(kSigningKeyLabel),
context,
kSigningKeySizeBits);
memcpy(kTestSigningKey, &signing_key[0], 32);
License::KeyContainer key_container;
key_container.set_id(content_key_id);
key_container.set_key(content_key);
key_container.set_type(License::KeyContainer::CONTENT);
return GenerateNewSignedLicense(license_request,
policies,
key_container,
encryption_key,
signing_key);
}
std::string GenerateLicenseRenewalResponse(
const SignedMessage& signed_message,
const PolicyType& policy_type) {
SessionState session_cache = GetTestSessionState();
LicenseRequest license_request;
if (!license_request.ParseFromString(signed_message.msg()))
return "";
std::string session_id = license_request.content_id().license().
license_id().session_id();
if (session_id.compare(session_cache.license_id().session_id()))
return "";
if (!VerifyTestSignature(signed_message.msg(),
signed_message.signature(),
session_cache.signing_key())) {
return "";
}
session_cache.mutable_license_id()->set_version(
session_cache.license_id().version() + 1);
License license;
license.mutable_id()->CopyFrom(session_cache.license_id());
// Always get Policy object with kDefault for renewal.
License::Policy policy;
GetPolicy(policy_type, &policy);
license.mutable_policy()->Swap(&policy);
license.set_license_start_time(GetCurrentTestTime());
return GenerateSignedLicenseResponse(license, session_cache.signing_key());
}
std::string GenerateLicenseResponse(const std::string& signed_request,
const PolicyType& policy) {
SignedMessage signed_message;
if (!signed_message.ParseFromString(signed_request))
return "";
LicenseRequest license_request;
if (!license_request.ParseFromString(signed_message.msg()))
return "";
if (license_request.type() == LicenseRequest::NEW) {
return GenerateNewLicenseResponse(license_request, policy);
} else if (license_request.type() == LicenseRequest::RENEWAL) {
return GenerateLicenseRenewalResponse(signed_message, policy);
}
return "";
}
struct WvCdmEncryptedData {
char plain_text[32];
int plain_text_size;
uint8_t key_id[32];
int key_id_size;
uint8_t content_key[32];
int content_key_size;
uint8_t encrypted_data[64];
int encrypted_data_size;
const char* license_response;
int license_response_size;
};
// Container used to pass data from GenerateLicenseRequest to Decrypt.
// TODO(rkuroiwa): This class was made before KeyMessage existed; this
// should be removed.
class LicenseRequestParameter {
public:
explicit LicenseRequestParameter(const WvCdmEncryptedData& frame)
: init_data(new uint8_t[frame.key_id_size]),
init_data_size(frame.key_id_size),
session_id(NULL),
session_id_size(0),
key_request(NULL),
key_request_size(0),
default_url(NULL),
default_url_size(0) {
memcpy(init_data.get(), frame.key_id, frame.key_id_size);
}
~LicenseRequestParameter() {
}
scoped_array<uint8_t> init_data;
int init_data_size;
scoped_array<char> session_id;
int session_id_size;
scoped_array<uint8_t> key_request;
int key_request_size;
scoped_array<char> default_url;
int default_url_size;
private:
DISALLOW_COPY_AND_ASSIGN(LicenseRequestParameter);
};
// |encrypted_data| is encrypted from |plain_text| using |key|. |key_id| is
// used to distinguish |key|.
struct WebmEncryptedData {
uint8 plain_text[32];
int plain_text_size;
uint8 key_id[32];
int key_id_size;
uint8 key[32];
int key_size;
uint8 encrypted_data[64];
int encrypted_data_size;
};
// Frames 0 & 1 are encrypted with the same key. Frame 2 is encrypted with a
// different key. Frame 3 is unencrypted.
const WebmEncryptedData kWebmEncryptedFrames[] = {
{
// plaintext
"Original data.", 14,
// key_id
{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13
}, 20,
// key
{ 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23
}, 16,
// encrypted_data
{ 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xf0, 0xd1, 0x12, 0xd5, 0x24, 0x81, 0x96,
0x55, 0x1b, 0x68, 0x9f, 0x38, 0x91, 0x85
}, 23
},
{
// plaintext
"Changed Original data.", 22,
// key_id
{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13
}, 20,
// key
{ 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23
}, 16,
// encrypted_data
{ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x57, 0x66, 0xf4, 0x12, 0x1a, 0xed, 0xb5,
0x79, 0x1c, 0x8e, 0x25, 0xd7, 0x17, 0xe7, 0x5e,
0x16, 0xe3, 0x40, 0x08, 0x27, 0x11, 0xe9
}, 31
},
{
// plaintext
"Original data.", 14,
// key_id
{ 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b,
0x2c, 0x2d, 0x2e, 0x2f, 0x30
}, 13,
// key
{ 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40
}, 16,
// encrypted_data
{ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x9c, 0x71, 0x26, 0x57, 0x3e, 0x25, 0x37,
0xf7, 0x31, 0x81, 0x19, 0x64, 0xce, 0xbc
}, 23
},
{
// plaintext
"Changed Original data.", 22,
// key_id
{ 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b,
0x2c, 0x2d, 0x2e, 0x2f, 0x30
}, 13,
// key
{ 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40
}, 16,
// encrypted_data
{ 0x00, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64,
0x20, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61,
0x6c, 0x20, 0x64, 0x61, 0x74, 0x61, 0x2e
}, 23
}
};
static const uint8 kWebmWrongSizedKey[] = { 0x20, 0x20 };
static const char kClearKeySystem[] = "org.w3.clearkey";
static const char *kWidevineKeySystem = "com.widevine.alpha";
static const char kKeyType[] = "any";
// All three ContentIdentification are supported but cenc_id only supports
// one key for now.
// Using key_id kCencTestRequest will return the content_key kCencTestRequest
//
static const char kCencTestRequest[] = "0123456789ABCDEF";
static const char kCencTestContentKey[] = {
0x16, 0x23, 0xa3, 0x16, 0x67, 0x6d, 0xb7, 0x70,
0xfe, 0x78, 0xf6, 0x58, 0x42, 0xb8, 0x16, 0x3c
};
// System Id of the Widevine DRM system for identification in pssh
static const uint8 kWidevineSystemId[] = {
0xED, 0xEF, 0x8B, 0xA9, 0x79, 0xD6, 0x4A, 0xCE,
0xA3, 0xC8, 0x27, 0xDC, 0xD5, 0x1D, 0x21, 0xED,
};
static std::string EncodeInt32(int i) {
std::string s;
s.resize(sizeof(int));
memcpy(&*s.begin(), &i, sizeof(int));
return s;
}
// Generate PSSH blob from init data
static std::string GeneratePSSHBlob(const uint8* init_data,
int init_data_length) {
std::string output;
// 4 byte size of the PSSH atom, inclusive
int size = 4 + 4 + 4 + sizeof(kWidevineSystemId) + 4 + init_data_length;
output.append(EncodeInt32(base::HostToNet32(size)));
// "pssh"
output.append("pssh");
// 4 byte flags, value 0
int flag = 0;
output.append(EncodeInt32(base::HostToNet32(flag)));
// 16 byte system id
output.append(reinterpret_cast<const char*>(kWidevineSystemId),
sizeof(kWidevineSystemId));
// 4 byte size of PSSH data, exclusive
output.append(EncodeInt32(base::HostToNet32(init_data_length)));
// pssh data
output.append(reinterpret_cast<const char*>(init_data),
init_data_length);
return output;
}
}; // anonymous
namespace wvcdm {
class WvCdmDecryptorTest : public testing::Test {
public:
WvCdmDecryptorTest() {}
~WvCdmDecryptorTest() {}
protected:
void GenerateKeyRequest(const uint8* key_id, int key_id_size,
std::string& message_buffer) {
std::string init_data = GeneratePSSHBlob(key_id, key_id_size);
wvcdm::CdmResponseType res = decryptor_.GenerateKeyRequest(
kWidevineKeySystem, init_data, &message_buffer, &session_id_string_);
EXPECT_TRUE(res == wvcdm::KEY_MESSAGE);
}
void PrepareForRenewalRequest(int i) {
}
void GetRenewalMessage(std::string& message_buffer) {
}
void GetFailedRenewalMessage(std::string& message_buffer) {
}
public:
void AddKeyAndExpectToSucceed(const uint8* key_id, int key_id_size,
const uint8* key, int key_size) {
std::string cma_key((const char*)key, key_size);
std::string init_data = GeneratePSSHBlob(key_id, key_id_size);
wvcdm::CdmResponseType res = decryptor_.AddKey(
kWidevineKeySystem, init_data, cma_key, session_id_string_);
EXPECT_TRUE(res == wvcdm::KEY_ADDED);
}
void AddKeyAndExpectToFail(const uint8* key_id, int key_id_size,
const uint8* key, int key_size) {
std::string cma_key((const char*)key, key_size);
std::string init_data = GeneratePSSHBlob(key_id, key_id_size);
wvcdm::CdmResponseType res = decryptor_.AddKey(
kWidevineKeySystem, init_data, cma_key, session_id_string_);
EXPECT_TRUE(res == wvcdm::KEY_ADDED);
}
protected:
wvcdm::WvContentDecryptionModule decryptor_;
std::string session_id_string_;
};
TEST_F(WvCdmDecryptorTest, RenewalTest) {
std::string response;
std::string message;
const WvCdmEncryptedFrameInfo& frame = kWvCdmEncryptedFrames[3];
License::Policy policies;
DCHECK(GetPolicy(frame.policy_type, &policies));
GenerateKeyRequest(frame.key_id, frame.key_id_size, message);
response = GenerateLicenseResponse(message, kShortDuration);
AddKeyAndExpectToSucceed(frame.key_id, frame.key_id_size,
reinterpret_cast<const uint8*>(response.data()),
response.size());
MessageLoop::current()->RunUntilIdle();
PrepareForRenewalRequest(1);
sleep(policies.renewal_delay_seconds());
MessageLoop::current()->RunUntilIdle();
GetRenewalMessage(message);
response = GenerateLicenseResponse(message, frame.policy_type);
AddKeyAndExpectToSucceed(frame.key_id, frame.key_id_size,
reinterpret_cast<const uint8*>(response.data()),
response.size());
}
TEST_F(WvCdmDecryptorTest, MultiRenewalTest) {
std::string response;
std::string message;
const WvCdmEncryptedFrameInfo& frame = kWvCdmEncryptedFrames[3];
License::Policy policies;
DCHECK(GetPolicy(frame.policy_type, &policies));
GenerateKeyRequest(frame.key_id, frame.key_id_size, message);
response = GenerateLicenseResponse(message, kShortDuration);
AddKeyAndExpectToSucceed(frame.key_id, frame.key_id_size,
reinterpret_cast<const uint8*>(response.data()),
response.size());
MessageLoop::current()->RunUntilIdle();
PrepareForRenewalRequest(1);
sleep(policies.renewal_delay_seconds());
MessageLoop::current()->RunUntilIdle();
GetRenewalMessage(message);
response = GenerateLicenseResponse(message, frame.policy_type);
AddKeyAndExpectToSucceed(frame.key_id, frame.key_id_size,
reinterpret_cast<const uint8*>(response.data()),
response.size());
PrepareForRenewalRequest(2);
sleep(policies.renewal_delay_seconds());
MessageLoop::current()->RunUntilIdle();
GetRenewalMessage(message);
}
TEST_F(WvCdmDecryptorTest, RenewalRetryTest_ExpectSuccess) {
std::string response;
std::string message;
const WvCdmEncryptedFrameInfo& frame = kWvCdmEncryptedFrames[3];
License::Policy policies;
DCHECK(GetPolicy(frame.policy_type, &policies));
GenerateKeyRequest(frame.key_id, frame.key_id_size, message);
response = GenerateLicenseResponse(message, kShortDuration);
AddKeyAndExpectToSucceed(frame.key_id, frame.key_id_size,
reinterpret_cast<const uint8*>(response.data()),
response.size());
MessageLoop::current()->RunUntilIdle();
int loop_seconds =
policies.license_duration_seconds() - policies.renewal_delay_seconds();
int loop_count = loop_seconds / policies.renewal_retry_interval_seconds();
if (loop_seconds % policies.renewal_retry_interval_seconds())
++loop_count;
for (int i = 1; i <= loop_count; ++i) {
PrepareForRenewalRequest(i);
sleep(policies.renewal_delay_seconds());
MessageLoop::current()->RunUntilIdle();
GetRenewalMessage(message);
}
response = GenerateLicenseResponse(message, frame.policy_type);
AddKeyAndExpectToSucceed(frame.key_id, frame.key_id_size,
reinterpret_cast<const uint8*>(response.data()),
response.size());
}
TEST_F(WvCdmDecryptorTest, RenewalRetryTest_ExpectLicenseExpiration) {
std::string response;
std::string message;
const WvCdmEncryptedFrameInfo& frame = kWvCdmEncryptedFrames[3];
License::Policy policies;
DCHECK(GetPolicy(frame.policy_type, &policies));
GenerateKeyRequest(frame.key_id, frame.key_id_size, message);
response = GenerateLicenseResponse(message, kShortDuration);
AddKeyAndExpectToSucceed(frame.key_id, frame.key_id_size,
reinterpret_cast<const uint8*>(response.data()),
response.size());
MessageLoop::current()->RunUntilIdle();
int loop_seconds =
policies.license_duration_seconds() - policies.renewal_delay_seconds();
int loop_count = loop_seconds / policies.renewal_retry_interval_seconds() + 1;
if (loop_seconds % policies.renewal_retry_interval_seconds())
++loop_count;
for (int i = 1; i <= loop_count; ++i) {
PrepareForRenewalRequest(i);
sleep(i > 1 ?
policies.renewal_retry_interval_seconds() :
policies.renewal_delay_seconds());
MessageLoop::current()->RunUntilIdle();
if (i < loop_count)
GetRenewalMessage(message);
}
GetFailedRenewalMessage(message);
}
} // namespace wvcdm
// TODO(rkuroiwa): Find where to put this main function just for Widevine CDM
// unit tests.
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
base::AtExitManager exit;
MessageLoop ttr(MessageLoop::TYPE_IO);
return RUN_ALL_TESTS();
}

View File

@@ -0,0 +1,201 @@
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <string>
#include "base/at_exit.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "string_conversions.h"
#include "wv_content_decryption_module.h"
namespace {
// Default license server to play server, can be configured using --server command line option
std::string gGpLicenseServer =
"https://jmt17.google.com/video-dev/license/GetCencLicense";
std::string gYtLicenseServer =
"https://www.youtube.com/api/drm/widevine?video_id=03681262dc412c06&source=YOUTUBE";
std::string gLicenseServer(gYtLicenseServer);
// Default key id (pssh), can be configured using --keyid command line option
std::string gKeyID = "000000347073736800000000" // blob size and pssh
"EDEF8BA979D64ACEA3C827DCD51D21ED00000014" // Widevine system id
"08011210e02562e04cd55351b14b3d748d36ed8e"; // pssh data
// An invalid key id, expected to fail
std::string kWrongKeyID = "000000347073736800000000" // blob size and psshb
"EDEF8BA979D64ACEA3C827DCD51D21ED00000014" // Widevine system id
"0901121094889920E8D6520098577DF8F2DD5546"; // pssh data
static const char kWidevineKeySystem[] = "com.widevine.alpha";
} // namespace
namespace wvcdm_test {
class WvCdmDecryptorTest : public testing::Test {
public:
WvCdmDecryptorTest() {}
~WvCdmDecryptorTest() {}
protected:
void GenerateKeyRequest(const std::string& key_system,
const std::string& init_data) {
EXPECT_EQ(decryptor_.GenerateKeyRequest(key_system,
init_data,
&key_msg_,
&session_id_), wvcdm::KEY_MESSAGE);
}
void GenerateRenewalRequest(const std::string& key_system,
const std::string& init_data) {
EXPECT_EQ(decryptor_.GenerateRenewalRequest(key_system,
init_data,
session_id_, &key_msg_),
wvcdm::KEY_MESSAGE);
}
std::string GetKeyRequestResponse(const std::string& server_url,
int expected_response) {
net::TestDelegate d;
net::TestNetworkDelegate network_delegate;
net::TestURLRequestContext context(true);
context.set_network_delegate(&network_delegate);
scoped_ptr<net::HostResolver> resolver(
net::HostResolver::CreateDefaultResolver(NULL));
context.set_host_resolver(resolver.get());
context.Init();
net::URLRequest r(GURL(server_url), &d, &context);
r.EnableChunkedUpload();
r.set_method("POST");
r.AppendChunkToUpload(key_msg_.data(), key_msg_.size(), true);
r.Start();
EXPECT_TRUE(r.is_pending());
MessageLoop::current()->Run();
std::string data = d.data_received();
// Youtube server returns 400 for invalid message while play server returns
// 500, so just test inequity here for invalid message
if (expected_response == 200) {
EXPECT_EQ(200, r.GetResponseCode()) << data;
} else {
EXPECT_NE(200, r.GetResponseCode()) << data;
}
EXPECT_EQ(net::URLRequestStatus::SUCCESS, r.status().status());
EXPECT_TRUE(d.bytes_received() > 0);
// Extract DRM message:
// https://docs.google.com/a/google.com/document/d/1Xue3bgwv2qIAnuFIZ-HCcix43dvH2UxsOEA_8FCBO3I/edit#
if (r.status().status() == net::URLRequestStatus::SUCCESS) {
size_t pos = data.find("\r\n");
if (pos != data.npos) data = data.substr(pos+2);
}
return data;
}
void VerifyKeyRequestResponse(const std::string& server_url,
std::string& init_data,
bool is_renewal) {
std::string resp = GetKeyRequestResponse(server_url, 200);
std::cout << "Message: " << wvcdm::b2a_hex(key_msg_) << std::endl;
std::cout << "Response: " << wvcdm::b2a_hex(resp) << std::endl;
if (is_renewal) {
EXPECT_EQ(decryptor_.RenewKey(kWidevineKeySystem,
init_data, resp,
session_id_), wvcdm::KEY_ADDED);
}
else {
EXPECT_EQ(decryptor_.AddKey(kWidevineKeySystem,
init_data, resp,
session_id_), wvcdm::KEY_ADDED);
}
std::cout << "back from AddKey" << std::endl;
}
wvcdm::WvContentDecryptionModule decryptor_;
std::string key_msg_;
std::string session_id_;
};
TEST_F(WvCdmDecryptorTest, BaseMessageTest)
{
GenerateKeyRequest(kWidevineKeySystem, gKeyID);
GetKeyRequestResponse(gLicenseServer, 200);
}
TEST_F(WvCdmDecryptorTest, WrongMessageTest)
{
std::string wrong_message = wvcdm::a2b_hex(kWrongKeyID);
GenerateKeyRequest(kWidevineKeySystem, wrong_message);
GetKeyRequestResponse(gLicenseServer, 500);
}
TEST_F(WvCdmDecryptorTest, NormalWebMDecryption) {
GenerateKeyRequest(kWidevineKeySystem, gKeyID);
VerifyKeyRequestResponse(gLicenseServer, gKeyID, false);
}
TEST_F(WvCdmDecryptorTest, LicenseRenewal) {
GenerateKeyRequest(kWidevineKeySystem, gKeyID);
VerifyKeyRequestResponse(gLicenseServer, gKeyID, false);
GenerateRenewalRequest(kWidevineKeySystem, gKeyID);
VerifyKeyRequestResponse(gLicenseServer, gKeyID, true);
}
} // namespace wvcdm_test
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
std::string temp;
std::string license_server(gLicenseServer);
std::string key_id(gKeyID);
for (int i=1; i<argc; i++) {
temp.assign(argv[i]);
if (temp.find("--server=") == 0) {
gLicenseServer.assign(temp.substr(strlen("--server=")));
}
else if (temp.find("--keyid=") == 0) {
gKeyID.assign(temp.substr(strlen("--keyid=")));
}
else {
std::cout << "error: unknown option '" << argv[i] << "'" << std::endl;
std::cout << "usage: wvcdm_test [options]" << std::endl << std::endl;
std::cout << std::setw(30) << std::left << " --server=<server_url>";
std::cout << "configure the license server url, please include http[s] in the url" << std::endl;
std::cout << std::setw(30) << std::left << " ";
std::cout << "default: " << license_server << std::endl;
std::cout << std::setw(30) << std::left << " --keyid=<key_id>";
std::cout << "configure the key id or pssh, in hex format" << std::endl;
std::cout << std::setw(30) << std::left << " ";
std::cout << "default: " << key_id << std::endl << std::endl;
return 0;
}
}
std::cout << std::endl;
std::cout << "Server: " << gLicenseServer << std::endl;
std::cout << "KeyID: " << gKeyID << std::endl << std::endl;
gKeyID = wvcdm::a2b_hex(gKeyID);
base::AtExitManager exit;
MessageLoop ttr(MessageLoop::TYPE_IO);
return RUN_ALL_TESTS();
}