diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c3dbfa2 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1587 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aes" +version = "0.9.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd4838e4ad37bb032dea137f441d5f71c16c26c068af512e64c5bc13a88cdfc7" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[package]] +name = "aquamarine" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a941c39708478e8eea39243b5983f1c42d2717b3620ee91f4a52115fd02ac43f" +dependencies = [ + "itertools 0.9.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "autocxx" +version = "0.30.0" +source = "git+https://github.com/tchebb/autocxx.git?branch=openwv-fixes#1fca5acd26f533576f98da45075d5498a1731d92" +dependencies = [ + "aquamarine", + "autocxx-macro", + "cxx", + "moveit", +] + +[[package]] +name = "autocxx-bindgen" +version = "0.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecaaf84d9cf1a772409c0abdac7d2477a31fd890dbdf606d8f684dc60129aa94" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.106", +] + +[[package]] +name = "autocxx-build" +version = "0.30.0" +source = "git+https://github.com/tchebb/autocxx.git?branch=openwv-fixes#1fca5acd26f533576f98da45075d5498a1731d92" +dependencies = [ + "autocxx-engine", + "env_logger 0.9.3", + "indexmap 1.9.3", + "syn 2.0.106", +] + +[[package]] +name = "autocxx-engine" +version = "0.30.0" +source = "git+https://github.com/tchebb/autocxx.git?branch=openwv-fixes#1fca5acd26f533576f98da45075d5498a1731d92" +dependencies = [ + "aquamarine", + "autocxx-bindgen", + "autocxx-parser", + "cc", + "cxx-gen", + "indexmap 1.9.3", + "indoc", + "itertools 0.10.5", + "log", + "miette", + "once_cell", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "regex_static", + "rustversion", + "serde_json", + "syn 2.0.106", + "tempfile", + "thiserror 1.0.69", + "version_check", +] + +[[package]] +name = "autocxx-macro" +version = "0.30.0" +source = "git+https://github.com/tchebb/autocxx.git?branch=openwv-fixes#1fca5acd26f533576f98da45075d5498a1731d92" +dependencies = [ + "autocxx-parser", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "autocxx-parser" +version = "0.30.0" +source = "git+https://github.com/tchebb/autocxx.git?branch=openwv-fixes#1fca5acd26f533576f98da45075d5498a1731d92" +dependencies = [ + "indexmap 1.9.3", + "itertools 0.10.5", + "log", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.106", + "thiserror 1.0.69", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bitflags" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" + +[[package]] +name = "block-buffer" +version = "0.11.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a229bfd78e4827c91b9b95784f69492c1b77c1ab75a45a8a037b139215086f94" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.4.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee88d14c41bbae2e333f574a27fc73d96fe1039e5a356c20d06a7f2a34cd8e5a" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cbc" +version = "0.2.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef95f543a56c245d9d0826ccbb34636ee983b3e846eff57bc5fc72e1bce1701" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" +dependencies = [ + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "cipher" +version = "0.5.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd4ef774202f1749465fc7cf88d70fc30620e8cacd5429268f4bff7d003bd976" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "cmac" +version = "0.8.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14088e66e0ff42509343b570d1c9c641bb4ddb972c43e606157fb6c571c334f0" +dependencies = [ + "cipher", + "dbl", + "digest", +] + +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "serde", + "termcolor", + "unicode-width 0.2.1", +] + +[[package]] +name = "const-oid" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dabb6555f92fb9ee4140454eb5dcd14c7960e1225c6d1a6cc361f032947713e" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.7.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737a2363b81de8cc95d8780d84aecb4b3c6f41e4473759da6636072b5514c875" +dependencies = [ + "num-traits", + "rand_core", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.2.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a23fa214dea9efd4dacee5a5614646b30216ae0f05d4bb51bafb50e9da1c5be" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "crypto-primes" +version = "0.7.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae744b9f528151f8c440cf67498f24d2d1ac0ab536b5ce7b1f87a7a5961bd1c1" +dependencies = [ + "crypto-bigint", + "libm", + "rand_core", +] + +[[package]] +name = "ctr" +version = "0.10.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f239edce204df0e4503cccef3492552773d1ca4e002659a59ca715f099b45ca1" +dependencies = [ + "cipher", +] + +[[package]] +name = "cxx" +version = "1.0.168" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa144b12f11741f0dab5b4182896afad46faa0598b6a061f7b9d17a21837ba7" +dependencies = [ + "cc", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash", + "link-cplusplus", +] + +[[package]] +name = "cxx-gen" +version = "0.7.168" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f1077f50ae47878fa3ae537e04e2ad8c4224f21fe3d40eb347109f57ef9e3c7" +dependencies = [ + "codespan-reporting", + "indexmap 2.10.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.168" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa36b7b249d43f67a3f54bd65788e35e7afe64bbc671396387a48b3e8aaea94" +dependencies = [ + "clap", + "codespan-reporting", + "indexmap 2.10.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.168" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77707c70f6563edc5429618ca34a07241b75ebab35bd01d46697c75d58f8ddfe" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.168" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede6c0fb7e318f0a11799b86ee29dcf17b9be2960bd379a6c38e1a96a6010fff" +dependencies = [ + "indexmap 2.10.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.106", +] + +[[package]] +name = "dbl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc736abf8a9eaf02a19b5b6a52c80560677d75e461e50e34d33b5e851f923ca" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "der" +version = "0.8.0-rc.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7050e8041c28720851f7db83183195b6acf375bb7bb28e3b86f0fe6cbd69459d" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.11.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460dd7f37e4950526b54a5a6b1f41b6c8e763c58eb9a8fc8fc05ba5c2f44ca7b" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", +] + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "env_filter", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hmac" +version = "0.13.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc6a2fcc35ab09136c6df2cdf9ca49790701420a3a6b5db0987dddbabc79b21" +dependencies = [ + "digest", +] + +[[package]] +name = "humantime" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" + +[[package]] +name = "hybrid-array" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d15931895091dea5c47afa5b3c9a01ba634b311919fd4d41388fa0e3d76af" +dependencies = [ + "typenum", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown 0.15.5", +] + +[[package]] +name = "indoc" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" + +[[package]] +name = "inout" +version = "0.2.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c774c86bce20ea04abe1c37cf0051c5690079a3a28ef5fdac2a5a0412b3d7d74" +dependencies = [ + "block-padding", + "hybrid-array", +] + +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "link-cplusplus" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6f6da007f968f9def0d65a05b187e2960183de70c160204ecfccf0ee330212" +dependencies = [ + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "miette" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" +dependencies = [ + "miette-derive", + "once_cell", + "thiserror 1.0.69", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "moveit" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87d7335204cb6ef7bd647fa6db0be3e4d7aa25b5823a7aa030027ddf512cefba" +dependencies = [ + "cxx", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openwv" +version = "1.1.3" +dependencies = [ + "aes", + "autocxx", + "autocxx-build", + "byteorder", + "cbc", + "cmac", + "ctr", + "cxx", + "env_logger 0.11.8", + "hmac", + "log", + "prost", + "prost-build", + "rand", + "rsa", + "sha1", + "sha2", + "thiserror 2.0.16", + "uuid", +] + +[[package]] +name = "pem-rfc7468" +version = "1.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e58fab693c712c0d4e88f8eb3087b6521d060bcaf76aeb20cb192d809115ba" +dependencies = [ + "base64ct", +] + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap 2.10.0", +] + +[[package]] +name = "pkcs1" +version = "0.8.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2345503b65d9be13aac96ddbec3eed60def8bc83869f9a519789afbcf3c2bea" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.11.0-rc.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c53e5d0804fa4070b1b2a5b320102f2c1c094920a7533d5d87c2630609bcbd34" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.106", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" +dependencies = [ + "heck", + "itertools 0.14.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.106", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "prost-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" +dependencies = [ + "prost", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "regex_static" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6126d61c5e4b41929098f73b42fc1d257116cc95d19739248c51591f77cc0021" +dependencies = [ + "once_cell", + "regex", + "regex_static_macro", +] + +[[package]] +name = "regex_static_impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3755019886a70e772e6360b0b58501d75cf7dc17a53e08aa97e59ecb2c2bc5" +dependencies = [ + "proc-macro2", + "quote", + "regex-syntax 0.6.29", + "syn 1.0.109", +] + +[[package]] +name = "regex_static_macro" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79b15495fd034158635bc8b762a132dfc83864d6992aeda1ffabf01b03b611a1" +dependencies = [ + "proc-macro2", + "regex_static_impl", + "syn 1.0.109", +] + +[[package]] +name = "rsa" +version = "0.10.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b311b61ea276fd51ccb77f6995c5f70084db933bab9655ba51fe5d831ea25b39" +dependencies = [ + "const-oid", + "crypto-bigint", + "crypto-primes", + "digest", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serdect" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42f67da2385b51a5f9652db9c93d78aeaf7610bf5ec366080b6de810604af53" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha1" +version = "0.11.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f9318facddf9ac32a33527066936837e189b3f23ced6edc1603720ead5e2b3d" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.11.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa1d2e6b3cc4e43a8258a9a3b17aa5dfd2cc5186c7024bba8a64aa65b2c71a59" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "3.0.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4835c3b5ecb10171941a4998a95a3a76ecac1c5ae8e6954f2ad030acd1c7e8ab" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "spki" +version = "0.8.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl 2.0.16", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "uuid" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ca68eea --- /dev/null +++ b/Cargo.toml @@ -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 "] +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" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0a04128 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. diff --git a/README.md b/README.md index d5967be..0ccbf59 100644 --- a/README.md +++ b/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 diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..6bab16b --- /dev/null +++ b/build.rs @@ -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(()) +} diff --git a/manifest-chromium.json b/manifest-chromium.json new file mode 100644 index 0000000..0d3891c --- /dev/null +++ b/manifest-chromium.json @@ -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" + ] +} diff --git a/manifest-firefox.json b/manifest-firefox.json new file mode 100644 index 0000000..157e03f --- /dev/null +++ b/manifest-firefox.json @@ -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" + ] +} diff --git a/src/common_cdm.h b/src/common_cdm.h new file mode 100644 index 0000000..0715030 --- /dev/null +++ b/src/common_cdm.h @@ -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_ diff --git a/src/common_host.rs b/src/common_host.rs new file mode 100644 index 0000000..c937e90 --- /dev/null +++ b/src/common_host.rs @@ -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( + 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); diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..8cbc9db --- /dev/null +++ b/src/config.rs @@ -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, +} diff --git a/src/content_key.rs b/src/content_key.rs new file mode 100644 index 0000000..8c4f7ab --- /dev/null +++ b/src/content_key.rs @@ -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>, + pub data: Vec, + pub key_type: Option, + pub track_label: Option, +} + +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::::from(t)), + }?; + if let Some(l) = &self.track_label { + write!(f, ": \"{l}\"")?; + } + write!(f, "]")?; + + Ok(()) + } +} diff --git a/src/decrypt.rs b/src/decrypt.rs new file mode 100644 index 0000000..aa2d583 --- /dev/null +++ b/src/decrypt.rs @@ -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::::new_from_slices(key.data.as_slice(), &padded_iv)? + } + 16 => ctr::Ctr64BE::::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::::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, + 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); + } +} diff --git a/src/init_data.rs b/src/init_data.rs new file mode 100644 index 0000000..94312b9 --- /dev/null +++ b/src/init_data.rs @@ -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 { + 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(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 . +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, 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)) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0d1f472 --- /dev/null +++ b/src/lib.rs @@ -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 + } +} diff --git a/src/license.rs b/src/license.rs new file mode 100644 index 0000000..7671a78 --- /dev/null +++ b/src/license.rs @@ -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) { + 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::::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, +) -> Result { + 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::(); + 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::::new_from_slices(&session_keys.encryption, &iv)?; + let new_size = decryptor + .decrypt_padded::(&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 { + let mut cmac = cmac::Cmac::::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, + }) +} diff --git a/src/openwv.rs b/src/openwv.rs new file mode 100644 index 0000000..85a40e2 --- /dev/null +++ b/src/openwv.rs @@ -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 = 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, + 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| cdm.As10().cast()), + 11 => (downcast_host::, |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::(), 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, + 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 = 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(); + } +} diff --git a/src/service_certificate.rs b/src/service_certificate.rs new file mode 100644 index 0000000..22bdec0 --- /dev/null +++ b/src/service_certificate.rs @@ -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, + 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::::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 { + 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 { + 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::::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 = (0..16).map(|_| rng.random()).collect(); + let privacy_iv: Vec = (0..16).map(|_| rng.random()).collect(); + + let client_id_encryptor = + cbc::Encryptor::::new_from_slices(&privacy_key, &privacy_iv).unwrap(); + + let encrypted_client_id = client_id_encryptor + .encrypt_padded_vec::( + client_id.encode_to_vec().as_slice(), + ); + + let rsa_padding = rsa::Oaep::new::(); + 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), + } +} diff --git a/src/session.rs b/src/session.rs new file mode 100644 index 0000000..5263804 --- /dev/null +++ b/src/session.rs @@ -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 { + let slice = unsafe { slice_from_c(ptr.cast::(), 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), + AwaitingLicense { request_bytes: Vec }, + Active, + Invalid, +} + +pub enum SessionEvent { + None, + Message(Vec), + KeysChange { new_keys: bool }, +} + +pub struct Session { + id: SessionId, + device: &'static WidevineDevice, + state: SessionState, + keys: Vec, +} + +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 { + 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); +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() + } +} diff --git a/src/signed_message.rs b/src/signed_message.rs new file mode 100644 index 0000000..a9cbc23 --- /dev/null +++ b/src/signed_message.rs @@ -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::::from(*.expected), EnumPrinter::::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 { + 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::::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(()) + } + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..c35cfc6 --- /dev/null +++ b/src/util.rs @@ -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 { + value: i32, + enum_type: PhantomData, +} + +impl From for EnumPrinter { + fn from(value: i32) -> Self { + EnumPrinter { + value, + enum_type: PhantomData, + } + } +} + +/// Print the enum variant name if known and the numeric value if not. +impl + Debug> Display for EnumPrinter { + 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 + Debug> Debug for EnumPrinter { + 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), + } + } +} diff --git a/src/wvd_file.rs b/src/wvd_file.rs new file mode 100644 index 0000000..22ee55a --- /dev/null +++ b/src/wvd_file.rs @@ -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 { + 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::()?; + 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::()?; + 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())?, + }) +} diff --git a/third-party/service_certificate_root.der b/third-party/service_certificate_root.der new file mode 100644 index 0000000..3a6bbfc Binary files /dev/null and b/third-party/service_certificate_root.der differ diff --git a/third-party/widevine_protos.pb b/third-party/widevine_protos.pb new file mode 100644 index 0000000..439da66 Binary files /dev/null and b/third-party/widevine_protos.pb differ