FORK
This commit is contained in:
parent
208282affd
commit
ae245326f4
1587
Cargo.lock
generated
Normal file
1587
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
Cargo.toml
Normal file
48
Cargo.toml
Normal file
@ -0,0 +1,48 @@
|
||||
[package]
|
||||
name = "openwv"
|
||||
version = "1.1.3"
|
||||
license = "LGPL-3.0-only"
|
||||
|
||||
description = "Open reimplementation of Google's Widevine Content Decryption Module for browsers"
|
||||
authors = ["Thomas Hebb <tommyhebb@gmail.com>"]
|
||||
repository = "https://github.com/tchebb/openwv"
|
||||
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
autocxx = { git = "https://github.com/tchebb/autocxx.git", branch = "openwv-fixes" }
|
||||
cxx = "1"
|
||||
prost = "0.14.1"
|
||||
thiserror = "2"
|
||||
log = "0.4"
|
||||
env_logger = { version = "0.11", default-features = false }
|
||||
byteorder = "1"
|
||||
rand = { version = "0.9", default-features = false, features = ["std", "os_rng"] }
|
||||
uuid = "1"
|
||||
|
||||
### RustCrypto crates
|
||||
rsa = "0.10.0-rc"
|
||||
sha1 = "0.11.0-rc"
|
||||
sha2 = "0.11.0-rc"
|
||||
cmac = "0.8.0-rc"
|
||||
hmac = "0.13.0-rc"
|
||||
aes = "0.9.0-rc"
|
||||
cbc = { version = "0.2.0-rc", features = ["alloc"] }
|
||||
ctr = "0.10.0-rc"
|
||||
|
||||
[build-dependencies]
|
||||
autocxx-build = { git = "https://github.com/tchebb/autocxx.git", branch = "openwv-fixes" }
|
||||
prost-build = "0.14.1"
|
||||
thiserror = "2"
|
||||
|
||||
[lib]
|
||||
name = "widevinecdm"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
strip = "symbols"
|
||||
lto = "fat"
|
||||
165
LICENSE
Normal file
165
LICENSE
Normal file
@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
112
README.md
112
README.md
@ -1,2 +1,112 @@
|
||||
# OpenWV
|
||||
OpenWV is a free and open-source reimplementation of Google's Widevine Content
|
||||
Decryption Module (CDM), the portion of the Widevine DRM system that runs in
|
||||
your browser, obtains content keys for protected media, and decrypts the media
|
||||
using those keys. OpenWV is a drop-in replacement for Google's [official,
|
||||
proprietary CDM][official-cdm] and implements the same [shared library
|
||||
API][chromium-cdm-api].
|
||||
|
||||
OpenWV does **not** come with a device identity and will not work without one.
|
||||
A device identity, typically stored as a [`.wvd` file][pywidevine], contains
|
||||
metadata about a Widevine client as well as a private key that authenticates
|
||||
that client to Widevine license servers. Some license servers return different
|
||||
sets of content keys to different clients: for example, many content providers
|
||||
encrypt high-definition content with a separate key and only give that key to
|
||||
device identities from hardware-backed ("L1") CDMs. If you want to use OpenWV,
|
||||
you must obtain an appropriate `.wvd` file yourself and include it in the build
|
||||
as described below.
|
||||
|
||||
[official-cdm]: https://github.com/mozilla-firefox/firefox/blob/main/toolkit/content/gmp-sources/widevinecdm.json
|
||||
|
||||
## Compilation
|
||||
|
||||
Because CDM libraries are heavily sandboxed by browsers, OpenWV cannot read
|
||||
configuration from disk at runtime. That means that all configuration,
|
||||
including the device identity mentioned above, must be present at build-time.
|
||||
As such, there are no official precompiled binaries: **the only way to use
|
||||
OpenWV is to build it yourself**.
|
||||
|
||||
To build OpenWV, follow these steps:
|
||||
|
||||
1. Make sure that [Git][git], [Rust][rust], and [Clang][clang-install] are
|
||||
installed on your system. (To install Clang on Windows 10/11, run
|
||||
`winget install LLVM.LLVM`.)
|
||||
2. Clone this repository and its submodule, telling Git to keep the two in sync:
|
||||
`git clone --recurse-submodules -c submodule.recurse=true https://github.com/tchebb/openwv.git`
|
||||
3. Place your `.wvd` file in the project root (alongside this README) and name
|
||||
it `embedded.wvd`. You may set other configuration options as desired by
|
||||
editing the `CONFIG` variable in `src/config.rs`.
|
||||
4. Build the library: `cargo build --release`
|
||||
5. Find the built library in `target/release/`. Depending on your OS, it will
|
||||
be named `libwidevinecdm.so`, `widevinecdm.dll`, or `libwidevinecdm.dylib`.
|
||||
|
||||
[git]: https://git-scm.com/downloads
|
||||
[rust]: https://rustup.rs/
|
||||
[clang-install]: https://rust-lang.github.io/rust-bindgen/requirements.html#installing-clang
|
||||
|
||||
## Installation
|
||||
|
||||
*NOTE: In these instructions, "the OpenWV library" means the library you built
|
||||
in the last section—`libwidevinecdm.so` on Linux, `widevinecdm.dll` on Windows,
|
||||
or `libwidevinecdm.dylib` on macOS.*
|
||||
|
||||
### Firefox
|
||||
1. Open `about:support` and note your "Profile Directory".
|
||||
2. Open `about:config`. Set `media.gmp-widevinecdm.autoupdate` to `false`
|
||||
(creating it if needed), and set `media.gmp-widevinecdm.version` to `openwv`
|
||||
(or to any other name for the directory you create in step 3).
|
||||
3. Navigate to `gmp-widevinecdm/` within your profile directory.
|
||||
4. Create a subdirectory named `openwv` and place the OpenWV library and
|
||||
`manifest-firefox.json`, renamed to `manifest.json`, inside it. Note that
|
||||
you **must** use OpenWV's `manifest.json` instead of Google's, as Firefox
|
||||
will not play video if we falsely advertise decoding support.
|
||||
|
||||
**If you manually check for addon updates, Firefox will replace OpenWV with
|
||||
Google's CDM**. The `media.gmp-widevinecdm.autoupdate` setting prevents
|
||||
automatic updates, but [there's no way][firefox-updater] to prevent manual
|
||||
updates. If this happens, set `media.gmp-widevinecdm.version` back to
|
||||
`openwv`—no need to repeat the other steps.
|
||||
|
||||
### Chrome/Chromium
|
||||
1. Open `chrome://version/` and note the **parent** directory of your "Profile
|
||||
Path". This is Chrome's "User Data Directory".
|
||||
2. Navigate to `WidevineCdm/` within the User Data Directory.
|
||||
3. If there are any existing subdirectories, delete them.
|
||||
4. Create a subdirectory named `9999` (or any numeric version greater than that
|
||||
of Google's CDM), and place OpenWV's `manifest-chromium.json`, renamed to
|
||||
`manifest.json`, inside it.
|
||||
5. Beside `manifest.json`, create a directory named `_platform_specific` with
|
||||
a directory named `{linux,win,mac}_{x86,x64,arm,arm64}`, as appropriate,
|
||||
inside it. For example, `_platform_specific/linux_x64/` on 64-bit Intel
|
||||
Linux. Place the OpenWV library in this innermost directory.
|
||||
6. On Linux only, launch and quit the browser once before playing any
|
||||
Widevine-protected media. OpenWV will not be loaded on the first launch due
|
||||
to an [implementation quirk][chromium-hint] of Chromium.
|
||||
|
||||
### Kodi (via [InputStream Adaptive](https://github.com/xbmc/inputstream.adaptive))
|
||||
1. Build OpenWV with `encrypt_client_id: EncryptClientId::Never`, as Kodi
|
||||
cannot handle service certificate request messages as of this writing
|
||||
(InputStream Adaptive v21.5.10).
|
||||
2. In Kodi, navigate to "Add-ons > My add-ons > VideoPlayer InputStream >
|
||||
InputStream Adaptive" and select "Configure".
|
||||
3. Ensure the settings level (the gear icon) is set to at least "Advanced".
|
||||
4. In the "Expert" tab, set "Decrypter path" to the directory where you've put
|
||||
the OpenWV library. Don't include the library name itself.
|
||||
|
||||
[firefox-updater]: https://github.com/mozilla-firefox/firefox/blob/FIREFOX_139_0_RELEASE/toolkit/mozapps/extensions/internal/GMPProvider.sys.mjs#L391-L455
|
||||
[chromium-hint]: https://source.chromium.org/chromium/chromium/src/+/refs/tags/137.0.7151.59:chrome/common/media/cdm_registration.cc;l=163-187
|
||||
|
||||
## References
|
||||
|
||||
The APIs, algorithms, and data types used in OpenWV were gathered from a
|
||||
variety of official and unofficial sources:
|
||||
|
||||
- API headers (`third-party/cdm/`) come from [the Chromium source][chromium-cdm-api].
|
||||
- Widevine protobuf definitions (`third-party/widevine_protos.pb`) were
|
||||
extracted from `chromecast_oss/chromium/src/out_chromecast_steak/release/pyproto/`
|
||||
in Google's [Chromecast Ultra v1.42 source drop][steak-1.42-oss].
|
||||
- The `.wvd` format and many algorithmic details come from the [pywidevine][pywidevine]
|
||||
project.
|
||||
|
||||
[chromium-cdm-api]: https://chromium.googlesource.com/chromium/cdm/
|
||||
[pywidevine]: https://github.com/devine-dl/pywidevine/
|
||||
[steak-1.42-oss]: https://drive.google.com/file/d/153TuZqh9FTBKRabGx686tbJefeqM2sJf/view?usp=drive_link
|
||||
|
||||
31
build.rs
Normal file
31
build.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
enum BuildError {
|
||||
#[error("autocxx failed in build.rs: {0}")]
|
||||
AutoCxxFailure(#[from] autocxx_build::BuilderError),
|
||||
#[error("prost failed in build.rs: {0}")]
|
||||
ProstFailure(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
fn main() -> Result<(), BuildError> {
|
||||
let bindings_rs = "src/lib.rs";
|
||||
let autocxx_incs = &[PathBuf::from("src"), PathBuf::from("third-party/cdm")];
|
||||
let mut autocxx = autocxx_build::Builder::new(bindings_rs, autocxx_incs)
|
||||
.extra_clang_args(&["-std=c++14"])
|
||||
.build()?;
|
||||
autocxx.std("c++14").compile("cdm-api");
|
||||
println!("cargo::rerun-if-changed={bindings_rs}");
|
||||
|
||||
let no_paths: [&str; 0] = [];
|
||||
let proto_fd = "third-party/widevine_protos.pb";
|
||||
prost_build::Config::new()
|
||||
.file_descriptor_set_path(proto_fd)
|
||||
.skip_protoc_run()
|
||||
.compile_protos(&no_paths, &no_paths)?;
|
||||
println!("cargo::rerun-if-changed={proto_fd}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
15
manifest-chromium.json
Normal file
15
manifest-chromium.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "WidevineCdm",
|
||||
"description": "OpenWV Widevine-compatible CDM",
|
||||
"version": "9999",
|
||||
"x-cdm-module-versions": "4",
|
||||
"x-cdm-interface-versions": "10,11",
|
||||
"x-cdm-host-versions": "10,11",
|
||||
"x-cdm-codecs": "vp8,vp09,avc1,av01",
|
||||
"x-cdm-persistent-license-support": false,
|
||||
"x-cdm-supported-encryption-schemes": [
|
||||
"cenc",
|
||||
"cbcs"
|
||||
]
|
||||
}
|
||||
15
manifest-firefox.json
Normal file
15
manifest-firefox.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "WidevineCdm",
|
||||
"description": "OpenWV Widevine-compatible CDM",
|
||||
"version": "1",
|
||||
"x-cdm-module-versions": "4",
|
||||
"x-cdm-interface-versions": "10,11",
|
||||
"x-cdm-host-versions": "10,11",
|
||||
"x-cdm-codecs": "",
|
||||
"x-cdm-persistent-license-support": false,
|
||||
"x-cdm-supported-encryption-schemes": [
|
||||
"cenc",
|
||||
"cbcs"
|
||||
]
|
||||
}
|
||||
73
src/common_cdm.h
Normal file
73
src/common_cdm.h
Normal file
@ -0,0 +1,73 @@
|
||||
#ifndef COMMON_CDM_H_
|
||||
#define COMMON_CDM_H_
|
||||
|
||||
#include "content_decryption_module.h"
|
||||
|
||||
namespace cdm {
|
||||
|
||||
class CDM_CLASS_API CommonCdm : public cdm::ContentDecryptionModule_10,
|
||||
public cdm::ContentDecryptionModule_11 {
|
||||
public:
|
||||
cdm::ContentDecryptionModule_10* As10() { return this; };
|
||||
cdm::ContentDecryptionModule_11* As11() { return this; };
|
||||
|
||||
void Initialize(bool allow_distinctive_identifier,
|
||||
bool allow_persistent_state,
|
||||
bool use_hw_secure_codecs) override = 0;
|
||||
void GetStatusForPolicy(uint32_t promise_id,
|
||||
const Policy& policy) override = 0;
|
||||
void SetServerCertificate(uint32_t promise_id,
|
||||
const uint8_t* server_certificate_data,
|
||||
uint32_t server_certificate_data_size) override = 0;
|
||||
void CreateSessionAndGenerateRequest(uint32_t promise_id,
|
||||
SessionType session_type,
|
||||
InitDataType init_data_type,
|
||||
const uint8_t* init_data,
|
||||
uint32_t init_data_size) override = 0;
|
||||
void LoadSession(uint32_t promise_id,
|
||||
SessionType session_type,
|
||||
const char* session_id,
|
||||
uint32_t session_id_size) override = 0;
|
||||
void UpdateSession(uint32_t promise_id,
|
||||
const char* session_id,
|
||||
uint32_t session_id_size,
|
||||
const uint8_t* response,
|
||||
uint32_t response_size) override = 0;
|
||||
void CloseSession(uint32_t promise_id,
|
||||
const char* session_id,
|
||||
uint32_t session_id_size) override = 0;
|
||||
void RemoveSession(uint32_t promise_id,
|
||||
const char* session_id,
|
||||
uint32_t session_id_size) override = 0;
|
||||
void TimerExpired(void* context) override = 0;
|
||||
Status Decrypt(const InputBuffer_2& encrypted_buffer,
|
||||
DecryptedBlock* decrypted_buffer) override = 0;
|
||||
Status InitializeAudioDecoder(
|
||||
const AudioDecoderConfig_2& audio_decoder_config) override = 0;
|
||||
Status InitializeVideoDecoder(
|
||||
const VideoDecoderConfig_2& video_decoder_config) override = 0;
|
||||
void DeinitializeDecoder(StreamType decoder_type) override = 0;
|
||||
void ResetDecoder(StreamType decoder_type) override = 0;
|
||||
Status DecryptAndDecodeFrame(const InputBuffer_2& encrypted_buffer,
|
||||
VideoFrame* video_frame) override = 0;
|
||||
Status DecryptAndDecodeSamples(const InputBuffer_2& encrypted_buffer,
|
||||
AudioFrames* audio_frames) override = 0;
|
||||
void OnPlatformChallengeResponse(
|
||||
const PlatformChallengeResponse& response) override = 0;
|
||||
void OnQueryOutputProtectionStatus(QueryResult result,
|
||||
uint32_t link_mask,
|
||||
uint32_t output_protection_mask) override =
|
||||
0;
|
||||
void OnStorageId(uint32_t version,
|
||||
const uint8_t* storage_id,
|
||||
uint32_t storage_id_size) override = 0;
|
||||
void Destroy() override = 0;
|
||||
|
||||
protected:
|
||||
CommonCdm() {}
|
||||
~CommonCdm() {}
|
||||
};
|
||||
|
||||
} // namespace cdm
|
||||
|
||||
#endif // COMMON_CDM_H_
|
||||
161
src/common_host.rs
Normal file
161
src/common_host.rs
Normal file
@ -0,0 +1,161 @@
|
||||
use std::ffi::{c_char, c_void};
|
||||
use std::pin::Pin;
|
||||
|
||||
use crate::ffi::cdm;
|
||||
|
||||
/// Trait abstracting over different `Host_NN` interface versions. Note that we
|
||||
/// only define the methods OpenWV actually uses, for brevity and better
|
||||
/// compatibility across versions.
|
||||
#[allow(non_snake_case)]
|
||||
pub trait CommonHost {
|
||||
fn Allocate(self: Pin<&mut Self>, capacity: u32) -> *mut cdm::Buffer;
|
||||
fn OnInitialized(self: Pin<&mut Self>, success: bool);
|
||||
fn OnResolveKeyStatusPromise(self: Pin<&mut Self>, promise_id: u32, key_status: cdm::KeyStatus);
|
||||
unsafe fn OnResolveNewSessionPromise(
|
||||
self: Pin<&mut Self>,
|
||||
promise_id: u32,
|
||||
session_id: *const c_char,
|
||||
session_id_size: u32,
|
||||
);
|
||||
fn OnResolvePromise(self: Pin<&mut Self>, promise_id: u32);
|
||||
unsafe fn OnRejectPromise(
|
||||
self: Pin<&mut Self>,
|
||||
promise_id: u32,
|
||||
exception: cdm::Exception,
|
||||
system_code: u32,
|
||||
error_message: *const c_char,
|
||||
error_message_size: u32,
|
||||
);
|
||||
unsafe fn OnSessionMessage(
|
||||
self: Pin<&mut Self>,
|
||||
session_id: *const c_char,
|
||||
session_id_size: u32,
|
||||
message_type: cdm::MessageType,
|
||||
message: *const c_char,
|
||||
message_size: u32,
|
||||
);
|
||||
unsafe fn OnSessionKeysChange(
|
||||
self: Pin<&mut Self>,
|
||||
session_id: *const c_char,
|
||||
session_id_size: u32,
|
||||
has_additional_usable_key: bool,
|
||||
keys_info: *const cdm::KeyInformation,
|
||||
keys_info_count: u32,
|
||||
);
|
||||
unsafe fn OnSessionClosed(
|
||||
self: Pin<&mut Self>,
|
||||
session_id: *const c_char,
|
||||
session_id_size: u32,
|
||||
);
|
||||
}
|
||||
|
||||
pub unsafe fn downcast_host<T: CommonHost + 'static>(
|
||||
ptr: *mut c_void,
|
||||
) -> Option<&'static mut dyn CommonHost> {
|
||||
let typed_ptr: *mut T = ptr.cast();
|
||||
let concrete_ref = unsafe { typed_ptr.as_mut() };
|
||||
concrete_ref.map(|x| x as &mut dyn CommonHost)
|
||||
}
|
||||
|
||||
macro_rules! impl_common_host {
|
||||
($target:path) => {
|
||||
impl CommonHost for $target {
|
||||
fn Allocate(self: Pin<&mut Self>, capacity: u32) -> *mut cdm::Buffer {
|
||||
self.Allocate(capacity)
|
||||
}
|
||||
|
||||
fn OnInitialized(self: Pin<&mut Self>, success: bool) {
|
||||
self.OnInitialized(success)
|
||||
}
|
||||
|
||||
fn OnResolveKeyStatusPromise(
|
||||
self: Pin<&mut Self>,
|
||||
promise_id: u32,
|
||||
key_status: cdm::KeyStatus,
|
||||
) {
|
||||
self.OnResolveKeyStatusPromise(promise_id, key_status)
|
||||
}
|
||||
|
||||
unsafe fn OnResolveNewSessionPromise(
|
||||
self: Pin<&mut Self>,
|
||||
promise_id: u32,
|
||||
session_id: *const c_char,
|
||||
session_id_size: u32,
|
||||
) {
|
||||
unsafe { self.OnResolveNewSessionPromise(promise_id, session_id, session_id_size) }
|
||||
}
|
||||
|
||||
fn OnResolvePromise(self: Pin<&mut Self>, promise_id: u32) {
|
||||
self.OnResolvePromise(promise_id)
|
||||
}
|
||||
|
||||
unsafe fn OnRejectPromise(
|
||||
self: Pin<&mut Self>,
|
||||
promise_id: u32,
|
||||
exception: cdm::Exception,
|
||||
system_code: u32,
|
||||
error_message: *const c_char,
|
||||
error_message_size: u32,
|
||||
) {
|
||||
unsafe {
|
||||
self.OnRejectPromise(
|
||||
promise_id,
|
||||
exception,
|
||||
system_code,
|
||||
error_message,
|
||||
error_message_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn OnSessionMessage(
|
||||
self: Pin<&mut Self>,
|
||||
session_id: *const c_char,
|
||||
session_id_size: u32,
|
||||
message_type: cdm::MessageType,
|
||||
message: *const c_char,
|
||||
message_size: u32,
|
||||
) {
|
||||
unsafe {
|
||||
self.OnSessionMessage(
|
||||
session_id,
|
||||
session_id_size,
|
||||
message_type,
|
||||
message,
|
||||
message_size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn OnSessionKeysChange(
|
||||
self: Pin<&mut Self>,
|
||||
session_id: *const c_char,
|
||||
session_id_size: u32,
|
||||
has_additional_usable_key: bool,
|
||||
keys_info: *const cdm::KeyInformation,
|
||||
keys_info_count: u32,
|
||||
) {
|
||||
unsafe {
|
||||
self.OnSessionKeysChange(
|
||||
session_id,
|
||||
session_id_size,
|
||||
has_additional_usable_key,
|
||||
keys_info,
|
||||
keys_info_count,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn OnSessionClosed(
|
||||
self: Pin<&mut Self>,
|
||||
session_id: *const c_char,
|
||||
session_id_size: u32,
|
||||
) {
|
||||
unsafe { self.OnSessionClosed(session_id, session_id_size) }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_common_host!(cdm::Host_10);
|
||||
impl_common_host!(cdm::Host_11);
|
||||
42
src/config.rs
Normal file
42
src/config.rs
Normal file
@ -0,0 +1,42 @@
|
||||
/// Compile-time configuration for OpenWV. Because we cannot access files
|
||||
/// outside the CDM sandbox, this holds various parameters that would typically
|
||||
/// go in a configuration file. See the comments on the structs and enums below
|
||||
/// for information on the meaning of each parameter.
|
||||
pub const CONFIG: OpenWvConfig = OpenWvConfig {
|
||||
widevine_device: include_bytes!("../embedded.wvd"),
|
||||
log_level: log::LevelFilter::Info,
|
||||
encrypt_client_id: EncryptClientId::Always,
|
||||
};
|
||||
|
||||
pub struct OpenWvConfig {
|
||||
/// A pywidevine `.wvd` file containing the private key and Client ID to
|
||||
/// present in license requests. You must obtain this on your own.
|
||||
pub widevine_device: &'static [u8],
|
||||
|
||||
/// This can be overridden by the OPENWV_LOG environment variable, but some
|
||||
/// browsers like Firefox don't let CDMs see the full environment.
|
||||
pub log_level: log::LevelFilter,
|
||||
|
||||
/// Policy for when to encrypt Client ID. Chrome uses `Always` if Verified
|
||||
/// Media Path is in use and `Never` otherwise. Similarly, ChromeOS uses
|
||||
/// `Always` if Platform Verification is enabled (i.e. when Developer Mode
|
||||
/// is off) and `Never` otherwise. The Android devices I've tested use
|
||||
/// `Always`. Chromecasts use `Never`.
|
||||
pub encrypt_client_id: EncryptClientId,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum EncryptClientId {
|
||||
/// Always send plaintext ClientIdentification, even if the application
|
||||
/// explicitly provided an encryption key with `setServerCertificate()`.
|
||||
Never,
|
||||
|
||||
/// Send encrypted ClientIdentification if the application called
|
||||
/// `setServerCertificate()`. Send plaintext otherwise.
|
||||
IfCertificateSet,
|
||||
|
||||
/// Always send encrypted ClientIdentification. If `setServerCertificate()`
|
||||
/// wasn't called, this results in an extra round trip to the license server
|
||||
/// to fetch a certificate.
|
||||
Always,
|
||||
}
|
||||
37
src/content_key.rs
Normal file
37
src/content_key.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::util::EnumPrinter;
|
||||
use crate::video_widevine::license::key_container::KeyType;
|
||||
|
||||
pub struct ContentKey {
|
||||
pub id: Option<Vec<u8>>,
|
||||
pub data: Vec<u8>,
|
||||
pub key_type: Option<i32>,
|
||||
pub track_label: Option<String>,
|
||||
}
|
||||
|
||||
impl Display for ContentKey {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if let Some(id) = &self.id {
|
||||
for b in id {
|
||||
write!(f, "{b:02x}")?;
|
||||
}
|
||||
write!(f, ":")?;
|
||||
}
|
||||
for b in &self.data {
|
||||
write!(f, "{b:02x}")?;
|
||||
}
|
||||
|
||||
write!(f, " [")?;
|
||||
match self.key_type {
|
||||
None => write!(f, "_"),
|
||||
Some(t) => write!(f, "{}", EnumPrinter::<KeyType>::from(t)),
|
||||
}?;
|
||||
if let Some(l) = &self.track_label {
|
||||
write!(f, ": \"{l}\"")?;
|
||||
}
|
||||
write!(f, "]")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
112
src/decrypt.rs
Normal file
112
src/decrypt.rs
Normal file
@ -0,0 +1,112 @@
|
||||
use aes::cipher::{BlockModeDecrypt, KeyIvInit, StreamCipher};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::content_key::ContentKey;
|
||||
use crate::ffi::cdm;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum DecryptError {
|
||||
#[error("key needed but not present")]
|
||||
NoKey,
|
||||
#[error("no iv provided for ciphered scheme")]
|
||||
NoIv,
|
||||
#[error("incorrect key or iv length")]
|
||||
BadKeyIvLength(#[from] aes::cipher::InvalidLength),
|
||||
#[error("integer overflow")]
|
||||
Overflow(#[from] std::num::TryFromIntError),
|
||||
#[error("subsamples exceed data length")]
|
||||
ShortData,
|
||||
}
|
||||
|
||||
pub fn decrypt_buf(
|
||||
key: Option<&ContentKey>,
|
||||
iv: Option<&[u8]>,
|
||||
data: &mut [u8],
|
||||
mode: cdm::EncryptionScheme,
|
||||
subsamples: Option<&[cdm::SubsampleEntry]>,
|
||||
pattern: &cdm::Pattern,
|
||||
) -> Result<(), DecryptError> {
|
||||
use cdm::EncryptionScheme::*;
|
||||
|
||||
match (mode, key, iv) {
|
||||
(kUnencrypted, _, _) => Ok(()),
|
||||
(kCenc, Some(key), Some(iv)) => {
|
||||
let mut decryptor = match iv.len() {
|
||||
len if len < 16 => {
|
||||
// IV is only 8 bytes for CTR mode. Chromium zero-pads it
|
||||
// to 16 bytes, but Firefox doesn't. Pad if needed.
|
||||
let mut padded_iv = [0u8; 16];
|
||||
padded_iv[..len].copy_from_slice(iv);
|
||||
ctr::Ctr64BE::<aes::Aes128>::new_from_slices(key.data.as_slice(), &padded_iv)?
|
||||
}
|
||||
16 => ctr::Ctr64BE::<aes::Aes128>::new_from_slices(key.data.as_slice(), iv)?,
|
||||
_ => return Err(aes::cipher::InvalidLength.into()),
|
||||
};
|
||||
|
||||
decrypt_possible_subsamples(data, subsamples, |ciphered| {
|
||||
decryptor.apply_keystream(ciphered);
|
||||
})
|
||||
}
|
||||
(kCbcs, Some(key), Some(iv)) => {
|
||||
let pattern_skip = usize::try_from(pattern.skip_byte_block)?;
|
||||
let mut pattern_crypt = usize::try_from(pattern.crypt_byte_block)?;
|
||||
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:media/cdm/cbcs_decryptor.cc;l=65-69;drc=2fdecb20631b358fed488a177af773d92f85d35c
|
||||
if pattern_skip == 0 && pattern_crypt == 0 {
|
||||
pattern_crypt = 1;
|
||||
}
|
||||
|
||||
let mut decryptor =
|
||||
cbc::Decryptor::<aes::Aes128>::new_from_slices(key.data.as_slice(), iv)?;
|
||||
|
||||
decrypt_possible_subsamples(data, subsamples, |ciphered| {
|
||||
decrypt_pattern(ciphered, &mut decryptor, pattern_skip, pattern_crypt);
|
||||
})
|
||||
}
|
||||
(_, None, _) => Err(DecryptError::NoKey),
|
||||
(_, _, None) => Err(DecryptError::NoIv),
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypt_possible_subsamples(
|
||||
data: &mut [u8],
|
||||
subsamples_opt: Option<&[cdm::SubsampleEntry]>,
|
||||
mut decrypt: impl FnMut(&mut [u8]),
|
||||
) -> Result<(), DecryptError> {
|
||||
// If there aren't any subsamples, our job is really easy.
|
||||
let Some(subsamples) = subsamples_opt else {
|
||||
decrypt(data);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut remaining = data;
|
||||
for subsample in subsamples {
|
||||
let ciphered_start = usize::try_from(subsample.clear_bytes)?;
|
||||
let ciphered_end = ciphered_start + usize::try_from(subsample.cipher_bytes)?;
|
||||
let ciphered = remaining
|
||||
.get_mut(ciphered_start..ciphered_end)
|
||||
.ok_or(DecryptError::ShortData)?;
|
||||
|
||||
decrypt(ciphered);
|
||||
|
||||
remaining = &mut remaining[ciphered_end..];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decrypt_pattern(
|
||||
data: &mut [u8],
|
||||
decryptor: &mut cbc::Decryptor<aes::Aes128>,
|
||||
pattern_skip: usize,
|
||||
pattern_crypt: usize,
|
||||
) {
|
||||
let mut blocks = data.chunks_exact_mut(16);
|
||||
while blocks.len() > 0 {
|
||||
for block in blocks.by_ref().take(pattern_crypt) {
|
||||
decryptor.decrypt_block(block.try_into().unwrap());
|
||||
}
|
||||
|
||||
blocks.by_ref().take(pattern_skip).for_each(drop);
|
||||
}
|
||||
}
|
||||
140
src/init_data.rs
Normal file
140
src/init_data.rs
Normal file
@ -0,0 +1,140 @@
|
||||
use byteorder::{BE, ByteOrder};
|
||||
use log::{info, warn};
|
||||
use rand::{Rng, TryRngCore};
|
||||
use thiserror::Error;
|
||||
use uuid::{Uuid, uuid};
|
||||
|
||||
use crate::CdmError;
|
||||
use crate::ffi::cdm::InitDataType;
|
||||
use crate::video_widevine::LicenseType;
|
||||
use crate::video_widevine::license_request::{ContentIdentification, content_identification};
|
||||
|
||||
// From https://dashif.org/identifiers/content_protection/
|
||||
const WIDEVINE_SYSTEMID: Uuid = uuid!("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed");
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum InitDataError {
|
||||
#[error("unsupported init data type")]
|
||||
UnsupportedType,
|
||||
#[error("no Widevine PSSH data in cenc init data")]
|
||||
NoValidPssh,
|
||||
#[error("unexpected end of data")]
|
||||
ShortData,
|
||||
#[error("box too large to parse")]
|
||||
Overflow(#[from] std::num::TryFromIntError),
|
||||
}
|
||||
|
||||
impl CdmError for InitDataError {
|
||||
fn cdm_exception(&self) -> crate::ffi::cdm::Exception {
|
||||
use crate::ffi::cdm::Exception::*;
|
||||
|
||||
match self {
|
||||
Self::UnsupportedType => kExceptionNotSupportedError,
|
||||
_ => kExceptionTypeError,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_data_to_content_id(
|
||||
init_data_type: InitDataType,
|
||||
init_data: &[u8],
|
||||
) -> Result<ContentIdentification, InitDataError> {
|
||||
let rng = rand::rngs::OsRng.unwrap_err();
|
||||
|
||||
// Note that CencDeprecated and WebmDeprecated seem to be required here,
|
||||
// despite their names. I tried using the newer InitData message, but the
|
||||
// license server I'm testing with rejects it.
|
||||
match init_data_type {
|
||||
InitDataType::kCenc => {
|
||||
let widevine_pssh_data = parse_cenc(init_data)?;
|
||||
|
||||
let proto = content_identification::CencDeprecated {
|
||||
pssh: vec![widevine_pssh_data.into()],
|
||||
license_type: Some(LicenseType::Streaming as i32),
|
||||
request_id: Some(rng.random_iter().take(16).collect()),
|
||||
};
|
||||
|
||||
Ok(ContentIdentification {
|
||||
cenc_id_deprecated: Some(proto),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
InitDataType::kWebM => {
|
||||
let proto = content_identification::WebmDeprecated {
|
||||
header: Some(init_data.into()),
|
||||
license_type: Some(LicenseType::Streaming as i32),
|
||||
request_id: Some(rng.random_iter().take(16).collect()),
|
||||
};
|
||||
|
||||
Ok(ContentIdentification {
|
||||
webm_id_deprecated: Some(proto),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
InitDataType::kKeyIds => Err(InitDataError::UnsupportedType),
|
||||
}
|
||||
}
|
||||
|
||||
fn checked_slice<I>(buf: &[u8], idx: I) -> Result<&I::Output, InitDataError>
|
||||
where
|
||||
I: std::slice::SliceIndex<[u8]>,
|
||||
{
|
||||
buf.get(idx).ok_or(InitDataError::ShortData)
|
||||
}
|
||||
|
||||
/// cenc-type init data holds "one or more concatenated Protection System Specific
|
||||
/// Header ('pssh') boxes", as per <https://www.w3.org/TR/eme-initdata-cenc/>.
|
||||
fn parse_cenc(boxes: &[u8]) -> Result<&[u8], InitDataError> {
|
||||
let mut remaining = boxes;
|
||||
|
||||
while !remaining.is_empty() {
|
||||
let mut box_size: u64 = BE::read_u32(checked_slice(remaining, 0..4)?).into();
|
||||
let box_type = checked_slice(remaining, 4..8)?;
|
||||
|
||||
let (payload_start, payload_end) = match box_size {
|
||||
// To end of file
|
||||
0 => (8, remaining.len()),
|
||||
// Extended size field
|
||||
1 => {
|
||||
box_size = BE::read_u64(checked_slice(remaining, 8..16)?);
|
||||
(16, box_size.try_into()?)
|
||||
}
|
||||
_ => (8, box_size.try_into()?),
|
||||
};
|
||||
let box_payload = checked_slice(remaining, payload_start..payload_end)?;
|
||||
|
||||
if box_type != b"pssh" {
|
||||
warn!(
|
||||
"Skipping unknown CENC box type: {}",
|
||||
box_type.escape_ascii()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(wv_pssh) = parse_pssh_box(box_payload)? {
|
||||
return Ok(wv_pssh);
|
||||
}
|
||||
|
||||
remaining = &remaining[payload_end..];
|
||||
}
|
||||
Err(InitDataError::NoValidPssh)
|
||||
}
|
||||
|
||||
fn parse_pssh_box(data: &[u8]) -> Result<Option<&[u8]>, InitDataError> {
|
||||
let version = *checked_slice(data, 0)?;
|
||||
if version != 0 {
|
||||
info!("Skipping PSSH box with unknown version {version}");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let system_id = Uuid::from_slice(checked_slice(data, 4..20)?).unwrap();
|
||||
if system_id != WIDEVINE_SYSTEMID {
|
||||
info!("Skipping PSSH box with non-Widevine system ID {system_id}");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let payload_size = BE::read_u32(checked_slice(data, 20..24)?);
|
||||
let payload = checked_slice(&data[24..], ..payload_size.try_into()?)?;
|
||||
Ok(Some(payload))
|
||||
}
|
||||
52
src/lib.rs
Normal file
52
src/lib.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use autocxx::include_cpp;
|
||||
|
||||
mod config;
|
||||
mod util;
|
||||
|
||||
mod common_host;
|
||||
mod content_key;
|
||||
mod decrypt;
|
||||
mod init_data;
|
||||
mod license;
|
||||
mod openwv;
|
||||
mod service_certificate;
|
||||
mod session;
|
||||
mod signed_message;
|
||||
mod wvd_file;
|
||||
|
||||
use openwv::OpenWv;
|
||||
include_cpp! {
|
||||
#include "common_cdm.h"
|
||||
safety!(unsafe)
|
||||
// FIXME: We can directly subclass from `cdm::ContentDecryptionModule_NN`
|
||||
// here if autocxx ever supports multiple inheritance for subclasses.
|
||||
subclass!("cdm::CommonCdm", OpenWv)
|
||||
generate!("cdm::Host_10")
|
||||
generate!("cdm::Host_11")
|
||||
generate!("cdm::Buffer")
|
||||
generate!("cdm::DecryptedBlock")
|
||||
generate_pod!("cdm::KeyInformation")
|
||||
generate_pod!("cdm::InputBuffer_2")
|
||||
generate_pod!("cdm::SubsampleEntry")
|
||||
generate_pod!("cdm::Pattern")
|
||||
}
|
||||
|
||||
// These are all just plain enums, totally safe to copy.
|
||||
impl Copy for ffi::cdm::Status {}
|
||||
impl Copy for ffi::cdm::Exception {}
|
||||
impl Copy for ffi::cdm::EncryptionScheme {}
|
||||
impl Copy for ffi::cdm::KeyStatus {}
|
||||
impl Copy for ffi::cdm::InitDataType {}
|
||||
impl Copy for ffi::cdm::SessionType {}
|
||||
impl Copy for ffi::cdm::MessageType {}
|
||||
|
||||
mod video_widevine {
|
||||
include!(concat!(env!("OUT_DIR"), "/video_widevine.rs"));
|
||||
}
|
||||
|
||||
pub trait CdmError {
|
||||
fn cdm_exception(&self) -> ffi::cdm::Exception;
|
||||
fn cdm_system_code(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
173
src/license.rs
Normal file
173
src/license.rs
Normal file
@ -0,0 +1,173 @@
|
||||
use aes::cipher::{BlockModeDecrypt, KeyIvInit};
|
||||
use byteorder::{BE, ByteOrder};
|
||||
use cmac::{Mac, digest::KeyInit};
|
||||
use log::info;
|
||||
use prost::Message;
|
||||
use rand::{Rng, TryRngCore};
|
||||
use rsa::signature::{RandomizedSigner, SignatureEncoding};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::content_key::ContentKey;
|
||||
use crate::service_certificate::{ServerCertificate, encrypt_client_id};
|
||||
use crate::util::now;
|
||||
use crate::video_widevine;
|
||||
use crate::wvd_file::WidevineDevice;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum LicenseError {
|
||||
#[error("bad license encapsulation: {0}")]
|
||||
BadSignedMessage(#[from] crate::signed_message::SignedMessageError),
|
||||
#[error("bad protobuf serialization")]
|
||||
BadProto(#[from] prost::DecodeError),
|
||||
#[error("no key in SignedMessage")]
|
||||
NoSessionKey,
|
||||
#[error("couldn't decrypt key: {0}")]
|
||||
BadSessionKeyCrypto(#[from] rsa::Error),
|
||||
#[error("incorrect key or iv length")]
|
||||
BadKeyIvLength(#[from] aes::cipher::InvalidLength),
|
||||
#[error("bad padding in content key")]
|
||||
BadContentKey(#[from] aes::cipher::block_padding::UnpadError),
|
||||
}
|
||||
|
||||
pub fn request_license(
|
||||
content_id: video_widevine::license_request::ContentIdentification,
|
||||
server_certificate: Option<&ServerCertificate>,
|
||||
device: &WidevineDevice,
|
||||
) -> (video_widevine::SignedMessage, Vec<u8>) {
|
||||
let mut rng = rand::rngs::OsRng.unwrap_err();
|
||||
let key_control_nonce: u32 = rng.random();
|
||||
|
||||
let mut req = video_widevine::LicenseRequest {
|
||||
content_id: Some(content_id),
|
||||
r#type: Some(video_widevine::license_request::RequestType::New as i32),
|
||||
request_time: Some(now()),
|
||||
protocol_version: Some(video_widevine::ProtocolVersion::Version21 as i32),
|
||||
key_control_nonce: Some(key_control_nonce),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match server_certificate {
|
||||
None => req.client_id = Some(device.client_id.clone()),
|
||||
Some(cert) => req.encrypted_client_id = Some(encrypt_client_id(cert, &device.client_id)),
|
||||
}
|
||||
|
||||
let req_bytes = req.encode_to_vec();
|
||||
|
||||
let signing_key = rsa::pss::SigningKey::<sha1::Sha1>::new(device.private_key.clone());
|
||||
let signature = signing_key.sign_with_rng(&mut rng, &req_bytes).to_vec();
|
||||
|
||||
let req_bytes_for_sig = req_bytes.clone();
|
||||
(
|
||||
video_widevine::SignedMessage {
|
||||
r#type: Some(video_widevine::signed_message::MessageType::LicenseRequest as i32),
|
||||
msg: Some(req_bytes),
|
||||
signature: Some(signature),
|
||||
..Default::default()
|
||||
},
|
||||
req_bytes_for_sig,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn load_license_keys(
|
||||
response_bytes: &[u8],
|
||||
request_bytes: &[u8],
|
||||
device: &WidevineDevice,
|
||||
keys: &mut Vec<ContentKey>,
|
||||
) -> Result<bool, LicenseError> {
|
||||
let response = video_widevine::SignedMessage::decode_with_type(
|
||||
response_bytes,
|
||||
video_widevine::signed_message::MessageType::License,
|
||||
)?;
|
||||
|
||||
let wrapped_key = response
|
||||
.session_key
|
||||
.as_ref()
|
||||
.ok_or(LicenseError::NoSessionKey)?;
|
||||
|
||||
let padding = rsa::Oaep::new::<sha1::Sha1>();
|
||||
let session_key = device.private_key.decrypt(padding, wrapped_key)?;
|
||||
let session_keys = derive_session_keys(request_bytes, &session_key)?;
|
||||
|
||||
response.verify_signature(&session_keys.mac_server)?;
|
||||
|
||||
let license = video_widevine::License::decode(response.msg_checked()?)?;
|
||||
|
||||
let mut added_keys = false;
|
||||
for key in license.key {
|
||||
let (Some(iv), Some(mut data)) = (key.iv, key.key) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let decryptor =
|
||||
cbc::Decryptor::<aes::Aes128>::new_from_slices(&session_keys.encryption, &iv)?;
|
||||
let new_size = decryptor
|
||||
.decrypt_padded::<aes::cipher::block_padding::Pkcs7>(&mut data)?
|
||||
.len();
|
||||
data.truncate(new_size);
|
||||
|
||||
let track_label = match key.track_label {
|
||||
Some(l) if l.is_empty() => None,
|
||||
x => x,
|
||||
};
|
||||
|
||||
let new_key = ContentKey {
|
||||
id: key.id,
|
||||
data,
|
||||
key_type: key.r#type,
|
||||
track_label,
|
||||
};
|
||||
|
||||
info!("Loaded key: {}", &new_key);
|
||||
keys.push(new_key);
|
||||
added_keys = true;
|
||||
}
|
||||
|
||||
Ok(added_keys)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SessionKeys {
|
||||
encryption: [u8; 16],
|
||||
mac_server: [u8; 32],
|
||||
#[allow(dead_code)]
|
||||
mac_client: [u8; 32],
|
||||
}
|
||||
|
||||
fn derive_session_keys(
|
||||
request_msg: &[u8],
|
||||
session_key: &[u8],
|
||||
) -> Result<SessionKeys, cmac::digest::InvalidLength> {
|
||||
let mut cmac = cmac::Cmac::<aes::Aes128>::new_from_slice(session_key)?;
|
||||
|
||||
let mut derive_key = |counter, label, key_size| {
|
||||
cmac.update(&[counter]);
|
||||
cmac.update(label);
|
||||
cmac.update(&[0]);
|
||||
cmac.update(request_msg);
|
||||
|
||||
let mut buf = [0u8; 4];
|
||||
BE::write_u32(&mut buf, key_size);
|
||||
cmac.update(&buf);
|
||||
|
||||
cmac.finalize_reset().into_bytes()
|
||||
};
|
||||
|
||||
let encryption = derive_key(1, b"ENCRYPTION", 128).into();
|
||||
|
||||
const AUTH_LABEL: &[u8] = b"AUTHENTICATION";
|
||||
|
||||
let mut mac_server = [0u8; 32];
|
||||
mac_server[..16].copy_from_slice(derive_key(1, AUTH_LABEL, 512).as_slice());
|
||||
mac_server[16..].copy_from_slice(derive_key(2, AUTH_LABEL, 512).as_slice());
|
||||
|
||||
let mut mac_client = [0u8; 32];
|
||||
mac_client[..16].copy_from_slice(derive_key(3, AUTH_LABEL, 512).as_slice());
|
||||
mac_client[16..].copy_from_slice(derive_key(4, AUTH_LABEL, 512).as_slice());
|
||||
|
||||
Ok(SessionKeys {
|
||||
encryption,
|
||||
mac_server,
|
||||
mac_client,
|
||||
})
|
||||
}
|
||||
533
src/openwv.rs
Normal file
533
src/openwv.rs
Normal file
@ -0,0 +1,533 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
143
src/service_certificate.rs
Normal file
143
src/service_certificate.rs
Normal file
@ -0,0 +1,143 @@
|
||||
use aes::cipher::{BlockModeEncrypt, KeyIvInit};
|
||||
use log::info;
|
||||
use prost::Message;
|
||||
use rand::{Rng, TryRngCore};
|
||||
use rsa::pkcs1::DecodeRsaPublicKey;
|
||||
use rsa::signature::Verifier;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::CdmError;
|
||||
use crate::ffi::cdm;
|
||||
use crate::util::EnumPrinter;
|
||||
use crate::video_widevine;
|
||||
use crate::video_widevine::drm_device_certificate::CertificateType;
|
||||
|
||||
const ROOT_PUBKEY: &[u8] = include_bytes!("../third-party/service_certificate_root.der");
|
||||
|
||||
/// This is like [`video_widevine::DrmDeviceCertificate`] but with no optional
|
||||
/// fields, for infallible Client ID encryption.
|
||||
pub struct ServerCertificate {
|
||||
key: rsa::RsaPublicKey,
|
||||
serial_number: Vec<u8>,
|
||||
provider_id: String,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum ServerCertificateError {
|
||||
#[error("certificate not present")]
|
||||
CertificateEmpty,
|
||||
#[error("bad certificate encapsulation: {0}")]
|
||||
BadSignedMessage(#[from] crate::signed_message::SignedMessageError),
|
||||
#[error("bad protobuf serialization")]
|
||||
BadProto(#[from] prost::DecodeError),
|
||||
#[error("missing protobuf fields")]
|
||||
MissingFields,
|
||||
#[error("could not verify signature")]
|
||||
BadSignature(#[from] rsa::signature::Error),
|
||||
#[error("couldn't parse certificate public key")]
|
||||
MalformedKey(#[from] rsa::pkcs1::Error),
|
||||
#[error("wrong certificate type {:?}", EnumPrinter::<CertificateType>::from(*.0))]
|
||||
WrongCertificateType(i32),
|
||||
}
|
||||
|
||||
// Only affects errors returned from SetServerCertificate(). UpdateSession()
|
||||
// errors are determined by SessionError's impl, even when the session message
|
||||
// contains a service certificate.
|
||||
impl CdmError for ServerCertificateError {
|
||||
fn cdm_exception(&self) -> cdm::Exception {
|
||||
match self {
|
||||
Self::CertificateEmpty => cdm::Exception::kExceptionTypeError,
|
||||
_ => cdm::Exception::kExceptionInvalidStateError,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_service_cert_message(
|
||||
message_bytes: &[u8],
|
||||
) -> Result<ServerCertificate, ServerCertificateError> {
|
||||
let message = video_widevine::SignedMessage::decode_with_type(
|
||||
message_bytes,
|
||||
video_widevine::signed_message::MessageType::ServiceCertificate,
|
||||
)?;
|
||||
|
||||
parse_service_certificate(Some(message.msg_checked()?))
|
||||
}
|
||||
|
||||
pub fn parse_service_certificate(
|
||||
server_certificate: Option<&[u8]>,
|
||||
) -> Result<ServerCertificate, ServerCertificateError> {
|
||||
let signed_cert_bytes = match server_certificate {
|
||||
None | Some(&[]) => return Err(ServerCertificateError::CertificateEmpty),
|
||||
Some(v) => v,
|
||||
};
|
||||
|
||||
let signed_cert = video_widevine::SignedDrmDeviceCertificate::decode(signed_cert_bytes)?;
|
||||
|
||||
let cert_bytes = signed_cert
|
||||
.drm_certificate
|
||||
.ok_or(ServerCertificateError::MissingFields)?;
|
||||
|
||||
let signature = rsa::pss::Signature::try_from(
|
||||
signed_cert
|
||||
.signature
|
||||
.ok_or(ServerCertificateError::MissingFields)?
|
||||
.as_slice(),
|
||||
)?;
|
||||
|
||||
let service_key = rsa::RsaPublicKey::from_pkcs1_der(ROOT_PUBKEY).unwrap();
|
||||
let verifying_key = rsa::pss::VerifyingKey::<sha1::Sha1>::new(service_key);
|
||||
verifying_key.verify(&cert_bytes, &signature)?;
|
||||
|
||||
let cert = video_widevine::DrmDeviceCertificate::decode(cert_bytes.as_slice())?;
|
||||
|
||||
let cert_type = cert.r#type.ok_or(ServerCertificateError::MissingFields)?;
|
||||
if cert_type != CertificateType::Service as i32 {
|
||||
return Err(ServerCertificateError::WrongCertificateType(cert_type));
|
||||
}
|
||||
|
||||
let res = ServerCertificate {
|
||||
key: rsa::RsaPublicKey::from_pkcs1_der(cert.public_key())?,
|
||||
serial_number: cert
|
||||
.serial_number
|
||||
.ok_or(ServerCertificateError::MissingFields)?,
|
||||
provider_id: cert
|
||||
.provider_id
|
||||
.ok_or(ServerCertificateError::MissingFields)?,
|
||||
};
|
||||
|
||||
info!("Service certificate provider: {}", res.provider_id);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn encrypt_client_id(
|
||||
cert: &ServerCertificate,
|
||||
client_id: &video_widevine::ClientIdentification,
|
||||
) -> video_widevine::EncryptedClientIdentification {
|
||||
let mut rng = rand::rngs::OsRng.unwrap_err();
|
||||
let privacy_key: Vec<u8> = (0..16).map(|_| rng.random()).collect();
|
||||
let privacy_iv: Vec<u8> = (0..16).map(|_| rng.random()).collect();
|
||||
|
||||
let client_id_encryptor =
|
||||
cbc::Encryptor::<aes::Aes128>::new_from_slices(&privacy_key, &privacy_iv).unwrap();
|
||||
|
||||
let encrypted_client_id = client_id_encryptor
|
||||
.encrypt_padded_vec::<aes::cipher::block_padding::Pkcs7>(
|
||||
client_id.encode_to_vec().as_slice(),
|
||||
);
|
||||
|
||||
let rsa_padding = rsa::Oaep::new::<sha1::Sha1>();
|
||||
let encrypted_privacy_key = cert
|
||||
.key
|
||||
.encrypt(&mut rng, rsa_padding, &privacy_key)
|
||||
.unwrap();
|
||||
|
||||
video_widevine::EncryptedClientIdentification {
|
||||
provider_id: Some(cert.provider_id.clone()),
|
||||
service_certificate_serial_number: Some(cert.serial_number.clone()),
|
||||
encrypted_client_id: Some(encrypted_client_id),
|
||||
encrypted_client_id_iv: Some(privacy_iv),
|
||||
encrypted_privacy_key: Some(encrypted_privacy_key),
|
||||
}
|
||||
}
|
||||
230
src/session.rs
Normal file
230
src/session.rs
Normal file
@ -0,0 +1,230 @@
|
||||
use prost::Message;
|
||||
use rand::{Rng, TryRngCore};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_char;
|
||||
use std::fmt::Display;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::CdmError;
|
||||
use crate::config::{CONFIG, EncryptClientId};
|
||||
use crate::content_key::ContentKey;
|
||||
use crate::ffi::cdm;
|
||||
use crate::init_data::{InitDataError, init_data_to_content_id};
|
||||
use crate::license::{LicenseError, load_license_keys, request_license};
|
||||
use crate::service_certificate::{
|
||||
ServerCertificate, ServerCertificateError, parse_service_cert_message,
|
||||
};
|
||||
use crate::util::slice_from_c;
|
||||
use crate::video_widevine;
|
||||
use crate::wvd_file::WidevineDevice;
|
||||
|
||||
/// Represents a session ID. We want this both to be copyable (so ideally
|
||||
/// entirely stack-allocated) and passable to C++ as a NUL-terminated string,
|
||||
/// which is why we do all this array to C string munging manually.
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct SessionId([u8; Self::LEN + 1]);
|
||||
|
||||
impl SessionId {
|
||||
const LEN: usize = 32;
|
||||
|
||||
fn generate() -> SessionId {
|
||||
// Technically, we can be any C string, but Google uses 32 characters
|
||||
// of uppercase hex.
|
||||
const CHARS: &[u8] = b"0123456789ABCDEF";
|
||||
|
||||
let dist = rand::distr::slice::Choose::new(CHARS).unwrap();
|
||||
let mut rng = rand::rngs::OsRng.unwrap_err();
|
||||
|
||||
let mut id = [0u8; Self::LEN + 1];
|
||||
|
||||
// Leave last element unfilled as NUL terminator
|
||||
for i in &mut id[..Self::LEN] {
|
||||
*i = *rng.sample(dist);
|
||||
}
|
||||
|
||||
SessionId(id)
|
||||
}
|
||||
|
||||
pub unsafe fn from_cxx(ptr: *const c_char, size: u32) -> Result<SessionId, BadSessionId> {
|
||||
let slice = unsafe { slice_from_c(ptr.cast::<std::ffi::c_uchar>(), size) }.unwrap();
|
||||
|
||||
if slice.len() != Self::LEN {
|
||||
return Err(BadSessionId);
|
||||
}
|
||||
|
||||
let mut id = [0u8; Self::LEN + 1];
|
||||
id[..Self::LEN].copy_from_slice(slice);
|
||||
|
||||
Ok(SessionId(id))
|
||||
}
|
||||
|
||||
pub fn as_cxx(&self) -> (*const c_char, u32) {
|
||||
(self.0.as_ptr().cast(), Self::LEN as _)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SessionId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0[..Self::LEN].escape_ascii())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error("invalid or non-existent session ID")]
|
||||
pub struct BadSessionId;
|
||||
impl CdmError for BadSessionId {
|
||||
fn cdm_exception(&self) -> cdm::Exception {
|
||||
cdm::Exception::kExceptionInvalidStateError
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum SessionError {
|
||||
#[error("update is not valid for state")]
|
||||
InvalidState,
|
||||
#[error("couldn't load server certificate: {0}")]
|
||||
ServiceCertError(#[from] ServerCertificateError),
|
||||
#[error("couldn't load license: {0}")]
|
||||
LicenseError(#[from] LicenseError),
|
||||
}
|
||||
|
||||
impl CdmError for SessionError {
|
||||
fn cdm_exception(&self) -> cdm::Exception {
|
||||
cdm::Exception::kExceptionTypeError
|
||||
}
|
||||
|
||||
fn cdm_system_code(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
enum SessionState {
|
||||
AwaitingServiceCert(Box<video_widevine::license_request::ContentIdentification>),
|
||||
AwaitingLicense { request_bytes: Vec<u8> },
|
||||
Active,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
pub enum SessionEvent {
|
||||
None,
|
||||
Message(Vec<u8>),
|
||||
KeysChange { new_keys: bool },
|
||||
}
|
||||
|
||||
pub struct Session {
|
||||
id: SessionId,
|
||||
device: &'static WidevineDevice,
|
||||
state: SessionState,
|
||||
keys: Vec<ContentKey>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn create(
|
||||
device: &'static WidevineDevice,
|
||||
init_data_type: cdm::InitDataType,
|
||||
init_data: &[u8],
|
||||
mut server_certificate: Option<&ServerCertificate>,
|
||||
) -> Result<(Self, SessionEvent), InitDataError> {
|
||||
// If we've been asked never to encrypt, pretend we weren't given a
|
||||
// server certificate.
|
||||
if let EncryptClientId::Never = CONFIG.encrypt_client_id {
|
||||
server_certificate = None;
|
||||
}
|
||||
|
||||
let content_id = init_data_to_content_id(init_data_type, init_data)?;
|
||||
let (msg, state) = match (CONFIG.encrypt_client_id, server_certificate) {
|
||||
(EncryptClientId::Always, None) => (
|
||||
video_widevine::SignedMessage {
|
||||
r#type: Some(
|
||||
video_widevine::signed_message::MessageType::ServiceCertificateRequest
|
||||
as i32,
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
SessionState::AwaitingServiceCert(Box::new(content_id)),
|
||||
),
|
||||
(_, cert) => {
|
||||
let (msg, request_bytes) = request_license(content_id, cert, device);
|
||||
(msg, SessionState::AwaitingLicense { request_bytes })
|
||||
}
|
||||
};
|
||||
|
||||
Ok((
|
||||
Session {
|
||||
id: SessionId::generate(),
|
||||
device,
|
||||
state,
|
||||
keys: vec![],
|
||||
},
|
||||
SessionEvent::Message(msg.encode_to_vec()),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: &[u8]) -> Result<SessionEvent, SessionError> {
|
||||
match std::mem::replace(&mut self.state, SessionState::Invalid) {
|
||||
SessionState::AwaitingServiceCert(cid) => {
|
||||
let cert = parse_service_cert_message(message)?;
|
||||
let (msg, request_bytes) = request_license(*cid, Some(&cert), self.device);
|
||||
self.state = SessionState::AwaitingLicense { request_bytes };
|
||||
Ok(SessionEvent::Message(msg.encode_to_vec()))
|
||||
}
|
||||
SessionState::AwaitingLicense { request_bytes } => {
|
||||
let new_keys =
|
||||
load_license_keys(message, &request_bytes, self.device, &mut self.keys)?;
|
||||
self.state = SessionState::Active;
|
||||
match new_keys {
|
||||
true => Ok(SessionEvent::KeysChange { new_keys: true }),
|
||||
false => Ok(SessionEvent::None),
|
||||
}
|
||||
}
|
||||
_ => Err(SessionError::InvalidState),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_licenses(&mut self) {
|
||||
self.keys.clear();
|
||||
}
|
||||
|
||||
pub fn id(&self) -> SessionId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn keys(&self) -> &[ContentKey] {
|
||||
&self.keys
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SessionStore(HashMap<SessionId, Session>);
|
||||
impl SessionStore {
|
||||
pub fn new() -> Self {
|
||||
Self(HashMap::new())
|
||||
}
|
||||
|
||||
pub fn add(&mut self, session: Session) {
|
||||
self.0.insert(session.id, session);
|
||||
}
|
||||
|
||||
pub unsafe fn lookup(
|
||||
&mut self,
|
||||
id: *const c_char,
|
||||
id_len: u32,
|
||||
) -> Result<&mut Session, BadSessionId> {
|
||||
let session_id = unsafe { SessionId::from_cxx(id, id_len) }.or(Err(BadSessionId))?;
|
||||
self.0.get_mut(&session_id).ok_or(BadSessionId)
|
||||
}
|
||||
|
||||
pub fn lookup_key(&self, id: &[u8]) -> Option<&ContentKey> {
|
||||
// A linear search of each session's keys is probably in practice
|
||||
// faster than a HashMap would be, given that we expect each session
|
||||
// to have on the order of 10 keys at most.
|
||||
self.0
|
||||
.values()
|
||||
.flat_map(|s| &s.keys)
|
||||
.find(|&k| k.id.as_ref().is_some_and(|x| x == id))
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, id: SessionId) -> bool {
|
||||
self.0.remove(&id).is_some()
|
||||
}
|
||||
}
|
||||
69
src/signed_message.rs
Normal file
69
src/signed_message.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use cmac::{Mac, digest::KeyInit};
|
||||
use prost::Message;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::util::EnumPrinter;
|
||||
use crate::video_widevine::SignedMessage;
|
||||
use crate::video_widevine::signed_message::MessageType;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum SignedMessageError {
|
||||
#[error("bad protobuf serialization")]
|
||||
BadProto(#[from] prost::DecodeError),
|
||||
#[error("no message type in SignedMessage")]
|
||||
NoMessageType,
|
||||
#[error("wrong message type: expected {:?}, got {:?}", EnumPrinter::<MessageType>::from(*.expected), EnumPrinter::<MessageType>::from(*.actual))]
|
||||
WrongMessageType { actual: i32, expected: i32 },
|
||||
#[error("no signature in SignedMessage")]
|
||||
NoSignature,
|
||||
#[error("couldn't verify signature")]
|
||||
BadSignature,
|
||||
#[error("no inner message in SignedMessage")]
|
||||
NoMessage,
|
||||
}
|
||||
|
||||
impl SignedMessage {
|
||||
pub fn decode_with_type(
|
||||
signed_message_bytes: &[u8],
|
||||
expected_type: MessageType,
|
||||
) -> Result<Self, SignedMessageError> {
|
||||
let type_idx = expected_type as i32;
|
||||
|
||||
let signed_message = SignedMessage::decode(signed_message_bytes)?;
|
||||
if signed_message.r#type != Some(type_idx) {
|
||||
return Err(signed_message.r#type.map_or(
|
||||
SignedMessageError::NoMessageType,
|
||||
|actual| SignedMessageError::WrongMessageType {
|
||||
actual,
|
||||
expected: type_idx,
|
||||
},
|
||||
));
|
||||
}
|
||||
Ok(signed_message)
|
||||
}
|
||||
|
||||
pub fn msg_checked(&self) -> Result<&[u8], SignedMessageError> {
|
||||
self.msg
|
||||
.as_ref()
|
||||
.ok_or(SignedMessageError::NoMessage)
|
||||
.map(Vec::as_slice)
|
||||
}
|
||||
|
||||
pub fn verify_signature(&self, key: &[u8; 32]) -> Result<(), SignedMessageError> {
|
||||
let mut digester = hmac::Hmac::<sha2::Sha256>::new_from_slice(key).unwrap();
|
||||
digester.update(self.msg_checked()?);
|
||||
let expected_sig = digester.finalize().into_bytes();
|
||||
|
||||
let actual_sig = self
|
||||
.signature
|
||||
.as_ref()
|
||||
.ok_or(SignedMessageError::NoSignature)?;
|
||||
|
||||
if actual_sig != expected_sig.as_slice() {
|
||||
Err(SignedMessageError::BadSignature)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
92
src/util.rs
Normal file
92
src/util.rs
Normal file
@ -0,0 +1,92 @@
|
||||
use std::ffi::CStr;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::io::Write;
|
||||
use std::marker::PhantomData;
|
||||
use std::slice::from_raw_parts;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::config::CONFIG;
|
||||
|
||||
pub fn try_init_logging() -> bool {
|
||||
let mut builder: env_logger::Builder = env_logger::Builder::new();
|
||||
|
||||
builder.format(|buf, record| {
|
||||
writeln!(
|
||||
buf,
|
||||
"[OpenWV {:<5}] {}",
|
||||
record.level(),
|
||||
record.args()
|
||||
)
|
||||
});
|
||||
|
||||
let env = env_logger::Env::new()
|
||||
.filter("OPENWV_LOG")
|
||||
.write_style("OPENWV_LOG_STYLE");
|
||||
|
||||
builder
|
||||
.filter_level(CONFIG.log_level)
|
||||
.write_style(env_logger::WriteStyle::Never)
|
||||
.parse_env(env)
|
||||
.try_init()
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub const fn cstr_from_str(str: &str) -> &CStr {
|
||||
match CStr::from_bytes_with_nul(str.as_bytes()) {
|
||||
Ok(str) => str,
|
||||
Err(_) => panic!("No NUL terminator in &str"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn now() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or(Duration::ZERO)
|
||||
.as_secs()
|
||||
.try_into()
|
||||
.unwrap_or(i64::MAX)
|
||||
}
|
||||
|
||||
pub unsafe fn slice_from_c<'a, T>(ptr: *const T, len: u32) -> Option<&'a [T]> {
|
||||
match ptr.is_null() {
|
||||
true => None,
|
||||
false => Some(unsafe { from_raw_parts(ptr, len.try_into().unwrap()) }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper type to print i32 enum values that are not guaranteed to fall within
|
||||
/// the known set of variants.
|
||||
pub struct EnumPrinter<T> {
|
||||
value: i32,
|
||||
enum_type: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> From<i32> for EnumPrinter<T> {
|
||||
fn from(value: i32) -> Self {
|
||||
EnumPrinter {
|
||||
value,
|
||||
enum_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Print the enum variant name if known and the numeric value if not.
|
||||
impl<T: TryFrom<i32> + Debug> Display for EnumPrinter<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match T::try_from(self.value) {
|
||||
Ok(v) => write!(f, "{v:?}"),
|
||||
Err(_) => write!(f, "{}", self.value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Print the numeric value followed by the enum variant name if known and
|
||||
/// "unknown variant" if not.
|
||||
impl<T: TryFrom<i32> + Debug> Debug for EnumPrinter<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match T::try_from(self.value) {
|
||||
Ok(v) => write!(f, "{} [{:?}]", self.value, v),
|
||||
Err(_) => write!(f, "{} [unknown variant]", self.value),
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/wvd_file.rs
Normal file
60
src/wvd_file.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use byteorder::{BE, ReadBytesExt};
|
||||
use prost::Message;
|
||||
use rsa::pkcs1::DecodeRsaPrivateKey;
|
||||
use std::io::Read;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::video_widevine::ClientIdentification;
|
||||
|
||||
pub struct WidevineDevice {
|
||||
pub private_key: rsa::RsaPrivateKey,
|
||||
pub client_id: ClientIdentification,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum WvdError {
|
||||
#[error("bad magic")]
|
||||
BadMagic,
|
||||
#[error("unsupported wvd version {0}")]
|
||||
UnsupportedVersion(u8),
|
||||
#[error("unexpected end of data")]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error("bad private key")]
|
||||
BadKey(#[from] rsa::pkcs1::Error),
|
||||
#[error("bad ClientIdentification serialization")]
|
||||
BadClientIdProto(#[from] prost::DecodeError),
|
||||
}
|
||||
|
||||
pub fn parse_wvd(wvd: &mut impl Read) -> Result<WidevineDevice, WvdError> {
|
||||
let mut magic = [0u8; 3];
|
||||
wvd.read_exact(&mut magic)?;
|
||||
|
||||
if magic != *b"WVD" {
|
||||
return Err(WvdError::BadMagic);
|
||||
}
|
||||
|
||||
let version = wvd.read_u8()?;
|
||||
if version != 1 && version != 2 {
|
||||
return Err(WvdError::UnsupportedVersion(version));
|
||||
}
|
||||
|
||||
let _type = wvd.read_u8()?;
|
||||
let _security_level = wvd.read_u8()?;
|
||||
let _flags = wvd.read_u8()?;
|
||||
|
||||
let private_key_len = wvd.read_u16::<BE>()?;
|
||||
let mut private_key_bytes = vec![];
|
||||
wvd.take(private_key_len.into())
|
||||
.read_to_end(&mut private_key_bytes)?;
|
||||
|
||||
let client_id_len = wvd.read_u16::<BE>()?;
|
||||
let mut client_id_bytes = vec![];
|
||||
wvd.take(client_id_len.into())
|
||||
.read_to_end(&mut client_id_bytes)?;
|
||||
|
||||
Ok(WidevineDevice {
|
||||
private_key: rsa::RsaPrivateKey::from_pkcs1_der(&private_key_bytes)?,
|
||||
client_id: ClientIdentification::decode(client_id_bytes.as_slice())?,
|
||||
})
|
||||
}
|
||||
BIN
third-party/service_certificate_root.der
vendored
Normal file
BIN
third-party/service_certificate_root.der
vendored
Normal file
Binary file not shown.
BIN
third-party/widevine_protos.pb
vendored
Normal file
BIN
third-party/widevine_protos.pb
vendored
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user