OpenWV/src/openwv.rs
2026-02-05 16:59:56 +02:00

534 lines
17 KiB
Rust

use autocxx::subclass::{CppSubclassSelfOwned, subclass};
use log::{debug, error, info, trace, warn};
use std::ffi::{c_char, c_int, c_uchar, c_void};
use std::pin::Pin;
use std::ptr::{null, null_mut};
use std::sync::OnceLock;
use crate::CdmError;
use crate::common_host::{CommonHost, downcast_host};
use crate::config::CONFIG;
use crate::decrypt::{DecryptError, decrypt_buf};
use crate::ffi::cdm;
use crate::service_certificate::{ServerCertificate, parse_service_certificate};
use crate::session::{Session, SessionEvent, SessionStore};
use crate::util::{cstr_from_str, slice_from_c, try_init_logging};
use crate::wvd_file;
// Holds the private key and client ID we use for license requests. Loaded once
// during InitializeCdmModule() and referenced by all subsequently-created
// Session structs.
static DEVICE: OnceLock<wvd_file::WidevineDevice> = OnceLock::new();
#[unsafe(no_mangle)]
extern "C" fn InitializeCdmModule_4() {
try_init_logging();
debug!("InitializeCdmModule()");
info!("OpenWV version {} initializing", env!("CARGO_PKG_VERSION"));
let mut embedded_wvd = std::io::Cursor::new(CONFIG.widevine_device);
match wvd_file::parse_wvd(&mut embedded_wvd) {
Ok(dev) => {
if DEVICE.set(dev).is_err() {
warn!("Tried to initialize CDM twice!");
}
}
Err(e) => error!("Could not parse embedded device: {e}"),
}
}
#[unsafe(no_mangle)]
extern "C" fn DeinitializeCdmModule() {
debug!("DeinitializeCdmModule()");
}
const WV_KEY_SYSTEM: &[u8] = b"com.widevine.alpha";
type GetCdmHostFunc = unsafe extern "C" fn(c_int, *mut c_void) -> *mut c_void;
#[unsafe(no_mangle)]
unsafe extern "C" fn CreateCdmInstance(
cdm_interface_version: c_int,
key_system: *const c_char,
key_system_size: u32,
get_cdm_host_func: Option<GetCdmHostFunc>,
user_data: *mut c_void,
) -> *mut c_void {
debug!("CreateCdmInstance()");
type IntoHost = unsafe fn(*mut c_void) -> Option<&'static mut dyn CommonHost>;
type FromCdm = fn(Pin<&mut cdm::CommonCdm>) -> *mut c_void;
let (into_host, from_cdm): (IntoHost, FromCdm) = match cdm_interface_version {
10 => (downcast_host::<cdm::Host_10>, |cdm| cdm.As10().cast()),
11 => (downcast_host::<cdm::Host_11>, |cdm| cdm.As11().cast()),
_ => {
error!("Unsupported interface {cdm_interface_version} requested");
return null_mut();
}
};
// SAFETY: The API contract requires that `key_system` be a valid pointer
// to a buffer of length `key_system_size`.
let Some(key_system_str) =
(unsafe { slice_from_c(key_system.cast::<c_uchar>(), key_system_size as _) })
else {
error!("Got NULL key_system pointer");
return null_mut();
};
if key_system_str != WV_KEY_SYSTEM {
error!(
"Unsupported key system '{}', expected '{}'",
key_system_str.escape_ascii(),
WV_KEY_SYSTEM.escape_ascii()
);
return null_mut();
}
// SAFETY: API contract requires that `get_cdm_host_func` returns an
// appropriate C++ Host_NN object.
let host_raw: *mut c_void = match get_cdm_host_func {
None => {
error!("Got NULL get_cdm_host_func pointer");
return null_mut();
}
Some(f) => unsafe { f(cdm_interface_version, user_data) },
};
// SAFETY: Although not explicitly documented, we can infer from the fact
// that the Host_NN class does not allow us to move or free it that this
// object remains owned by C++. As such, we only want a reference.
let host = match unsafe { into_host(host_raw) } {
None => {
error!("No host functions available");
return null_mut();
}
// SAFETY: Objects owned by C++ never move.
Some(p) => unsafe { Pin::new_unchecked(p) },
};
let Some(device) = DEVICE.get() else {
error!("Called CreateCdmInstance() before initializing module");
return null_mut();
};
let openwv = OpenWv::new_self_owned(OpenWv {
host,
sessions: SessionStore::new(),
device,
server_cert: None,
allow_persistent_state: false,
cpp_peer: Default::default(),
});
let mut openwv_ref = openwv.borrow_mut();
let res = from_cdm(openwv_ref.pin_mut());
info!("Created CDM with interface {cdm_interface_version}");
res
}
const VERSION_STR: &std::ffi::CStr =
cstr_from_str(concat!("OpenWV version ", env!("CARGO_PKG_VERSION"), "\0"));
#[unsafe(no_mangle)]
extern "C" fn GetCdmVersion() -> *const c_char {
VERSION_STR.as_ptr()
}
// FIXME: This is needed because autocxx's `#[subclass]` currently hardcodes an
// `ffi::` module prefix. If autocxx gets more hygienic, we should remove this.
use crate::ffi;
#[subclass(self_owned)]
pub struct OpenWv {
host: Pin<&'static mut dyn CommonHost>,
sessions: SessionStore,
device: &'static wvd_file::WidevineDevice,
server_cert: Option<ServerCertificate>,
allow_persistent_state: bool,
}
impl dyn CommonHost {
fn reject(
self: Pin<&mut Self>,
promise_id: u32,
exception: cdm::Exception,
msg: &std::ffi::CStr,
) {
unsafe {
self.OnRejectPromise(
promise_id,
exception,
0,
msg.as_ptr(),
msg.count_bytes().try_into().unwrap(),
);
}
}
fn throw(self: Pin<&mut Self>, promise_id: u32, e: &(impl std::error::Error + CdmError)) {
warn!("Returning API error: {e}");
// SAFETY: This CString cannot be a temporary, as it must live until
// after the OnRejectPromise() FFI call.
let msg_str = std::ffi::CString::new(e.to_string()).ok();
let (msg_ptr, msg_size) = match &msg_str {
None => (null(), 0),
Some(s) => (s.as_ptr(), s.count_bytes()),
};
unsafe {
self.OnRejectPromise(
promise_id,
e.cdm_exception(),
e.cdm_system_code(),
msg_ptr,
msg_size.try_into().unwrap(),
);
}
}
}
fn process_event(event: SessionEvent, session: &Session, mut host: Pin<&mut dyn CommonHost>) {
// SAFETY: This SessionId cannot be a temporary, as it must live until after
// the various FFI calls below.
let session_id = session.id();
let (id_ptr, id_len) = session_id.as_cxx();
match event {
SessionEvent::Message(request) => unsafe {
host.as_mut().OnSessionMessage(
id_ptr,
id_len,
cdm::MessageType::kLicenseRequest,
request.as_ptr().cast(),
request.len() as _,
);
},
SessionEvent::KeysChange { new_keys } => {
// Build an array of KeyInformation structs that point into keys.
let key_infos: Vec<cdm::KeyInformation> = session
.keys()
.iter()
.filter_map(|k| {
k.id.as_ref().map(|id| cdm::KeyInformation {
key_id: id.as_ptr(),
key_id_size: id.len() as _,
status: cdm::KeyStatus::kUsable,
system_code: 0,
})
})
.collect();
unsafe {
host.as_mut().OnSessionKeysChange(
id_ptr,
id_len,
new_keys,
key_infos.as_ptr(),
key_infos.len() as _,
);
}
}
_ => (),
}
}
impl cdm::CommonCdm_methods for OpenWv {
fn Initialize(
&mut self,
_allow_distinctive_identifier: bool,
allow_persistent_state: bool,
_use_hw_secure_codecs: bool,
) {
debug!("OpenWv({self:p}).Initialize()");
self.allow_persistent_state = allow_persistent_state;
self.host.as_mut().OnInitialized(true);
}
fn GetStatusForPolicy(&mut self, promise_id: u32, _policy: &cdm::Policy) {
debug!("OpenWv({self:p}).GetStatusForPolicy()");
self.host
.as_mut()
.OnResolveKeyStatusPromise(promise_id, cdm::KeyStatus::kUsable);
}
unsafe fn SetServerCertificate(
&mut self,
promise_id: u32,
server_certificate_data: *const u8,
server_certificate_data_size: u32,
) {
debug!("OpenWv({self:p}).SetServerCertificate()");
let server_certificate =
unsafe { slice_from_c(server_certificate_data, server_certificate_data_size) };
match parse_service_certificate(server_certificate) {
Ok(cert) => {
self.server_cert = Some(cert);
self.host.as_mut().OnResolvePromise(promise_id);
}
Err(e) => self.host.as_mut().throw(promise_id, &e),
}
}
unsafe fn CreateSessionAndGenerateRequest(
&mut self,
promise_id: u32,
session_type: cdm::SessionType,
init_data_type: cdm::InitDataType,
init_data_raw: *const u8,
init_data_size: u32,
) {
debug!("OpenWv({self:p}).CreateSessionAndGenerateRequest()");
if session_type == cdm::SessionType::kPersistentLicense && !self.allow_persistent_state {
self.host.as_mut().reject(
promise_id,
cdm::Exception::kExceptionNotSupportedError,
c"persistent state not allowed",
);
return;
}
let init_data = unsafe { slice_from_c(init_data_raw, init_data_size) }.unwrap();
match Session::create(
self.device,
init_data_type,
init_data,
self.server_cert.as_ref(),
) {
Ok((sess, result)) => {
let session_id = sess.id();
let (id_ptr, id_len) = session_id.as_cxx();
unsafe {
self.host
.as_mut()
.OnResolveNewSessionPromise(promise_id, id_ptr, id_len);
}
process_event(result, &sess, self.host.as_mut());
self.sessions.add(sess);
info!("Registered new session {session_id}");
}
Err(e) => self.host.as_mut().throw(promise_id, &e),
}
}
unsafe fn LoadSession(
&mut self,
promise_id: u32,
_session_type: cdm::SessionType,
_session_id: *const c_char,
_session_id_size: u32,
) {
debug!("OpenWv({self:p}).LoadSession()");
// TODO: Implement
self.host.as_mut().reject(
promise_id,
cdm::Exception::kExceptionNotSupportedError,
c"no persistent sessions",
);
}
unsafe fn UpdateSession(
&mut self,
promise_id: u32,
session_id: *const c_char,
session_id_size: u32,
response_raw: *const u8,
response_size: u32,
) {
debug!("OpenWv({self:p}).UpdateSession()");
let sess = match unsafe { self.sessions.lookup(session_id, session_id_size) } {
Ok(s) => s,
Err(e) => {
self.host.as_mut().throw(promise_id, &e);
return;
}
};
let response = unsafe { slice_from_c(response_raw, response_size as _) }.unwrap();
match sess.update(response) {
Ok(result) => {
// This order matches Google's CDM: resolve the promise first,
// then send the key change event.
self.host.as_mut().OnResolvePromise(promise_id);
process_event(result, sess, self.host.as_mut());
}
Err(e) => self.host.as_mut().throw(promise_id, &e),
}
}
unsafe fn CloseSession(
&mut self,
promise_id: u32,
session_id: *const c_char,
session_id_size: u32,
) {
debug!("OpenWv({self:p}).CloseSession()");
match unsafe { self.sessions.lookup(session_id, session_id_size) } {
Ok(s) => {
let id = s.id();
self.sessions.delete(id);
info!("Deleted session {id}");
self.host.as_mut().OnResolvePromise(promise_id);
unsafe {
self.host
.as_mut()
.OnSessionClosed(session_id, session_id_size);
}
}
Err(e) => self.host.as_mut().throw(promise_id, &e),
};
}
unsafe fn RemoveSession(
&mut self,
promise_id: u32,
session_id: *const c_char,
session_id_size: u32,
) {
debug!("OpenWv({self:p}).RemoveSession()");
match unsafe { self.sessions.lookup(session_id, session_id_size) } {
Ok(s) => {
s.clear_licenses();
self.host.as_mut().OnResolvePromise(promise_id);
}
Err(e) => self.host.as_mut().throw(promise_id, &e),
};
}
unsafe fn TimerExpired(&mut self, _context: *mut autocxx::c_void) {
debug!("OpenWv({self:p}).TimerExpired()");
warn!("Got TimerExpired(), but we never called SetTimer()!");
}
unsafe fn Decrypt(
&mut self,
in_buf: &cdm::InputBuffer_2,
out_block_raw: *mut cdm::DecryptedBlock,
) -> cdm::Status {
trace!("OpenWv({self:p}).Decrypt()");
let mut out_block = match unsafe { out_block_raw.as_mut() } {
None => return cdm::Status::kSuccess,
Some(p) => unsafe { Pin::new_unchecked(p) },
};
// Output will always be the same size as input, so let's do the unsafe
// allocation here and copy from in_buf to get an initialized slice
// decrypt_buf() can modify in-place.
let out_buf_raw = self.host.as_mut().Allocate(in_buf.data_size);
let mut out_buf = match unsafe { out_buf_raw.as_mut() } {
None => return cdm::Status::kDecryptError,
Some(p) => unsafe { Pin::new_unchecked(p) },
};
// SAFETY: Allocation may be uninitialized, so from_raw_parts_mut() is
// only safe after we initialize it.
let out_data_raw = out_buf.as_mut().Data();
let data_len = usize::try_from(in_buf.data_size).unwrap();
let data = unsafe {
out_data_raw.copy_from_nonoverlapping(in_buf.data, data_len);
std::slice::from_raw_parts_mut(out_data_raw, data_len)
};
out_buf.as_mut().SetSize(in_buf.data_size);
let key_id = unsafe { slice_from_c(in_buf.key_id, in_buf.key_id_size) };
let iv = unsafe { slice_from_c(in_buf.iv, in_buf.iv_size) };
let subsamples = unsafe { slice_from_c(in_buf.subsamples, in_buf.num_subsamples) };
let key = key_id.and_then(|v| self.sessions.lookup_key(v));
match decrypt_buf(
key,
iv,
data,
in_buf.encryption_scheme,
subsamples,
&in_buf.pattern,
) {
Ok(()) => {
unsafe { out_block.as_mut().SetDecryptedBuffer(out_buf_raw) };
out_block.as_mut().SetTimestamp(in_buf.timestamp);
cdm::Status::kSuccess
}
Err(DecryptError::NoKey) => {
out_buf.as_mut().Destroy();
cdm::Status::kNoKey
}
Err(e) => {
warn!("Decryption error: {e}");
out_buf.as_mut().Destroy();
cdm::Status::kDecryptError
}
}
}
fn InitializeAudioDecoder(
&mut self,
_audio_decoder_config: &cdm::AudioDecoderConfig_2,
) -> cdm::Status {
debug!("OpenWv({self:p}).InitializeAudioDecoder()");
cdm::Status::kInitializationError
}
fn InitializeVideoDecoder(
&mut self,
_video_decoder_config: &cdm::VideoDecoderConfig_2,
) -> cdm::Status {
debug!("OpenWv({self:p}).InitializeVideoDecoder()");
cdm::Status::kInitializationError
}
fn DeinitializeDecoder(&mut self, _decoder_type: cdm::StreamType) {
debug!("OpenWv({self:p}).DeinitializeDecoder()");
}
fn ResetDecoder(&mut self, _decoder_type: cdm::StreamType) {
debug!("OpenWv({self:p}).ResetDecoder()");
}
unsafe fn DecryptAndDecodeFrame(
&mut self,
_encrypted_buffer: &cdm::InputBuffer_2,
_video_frame: *mut cdm::VideoFrame,
) -> cdm::Status {
debug!("OpenWv({self:p}).DecryptAndDecodeFrame()");
cdm::Status::kDecodeError
}
unsafe fn DecryptAndDecodeSamples(
&mut self,
_encrypted_buffer: &cdm::InputBuffer_2,
_audio_frames: *mut cdm::AudioFrames,
) -> cdm::Status {
debug!("OpenWv({self:p}).DecryptAndDecodeSamples()");
cdm::Status::kDecodeError
}
fn OnPlatformChallengeResponse(&mut self, _response: &cdm::PlatformChallengeResponse) {
debug!("OpenWv({self:p}).OnPlatformChallengeResponse()");
}
fn OnQueryOutputProtectionStatus(
&mut self,
_result: cdm::QueryResult,
_link_mask: u32,
_output_protection_mask: u32,
) {
debug!("OpenWv({self:p}).OnQueryOutputProtectionStatus()");
}
unsafe fn OnStorageId(&mut self, _version: u32, _storage_id: *const u8, _storage_id_size: u32) {
debug!("OpenWv({self:p}).OnStorageId()");
}
fn Destroy(&mut self) {
self.delete_self();
}
}