diff options
author | Arun Raghavan <arun@asymptotic.io> | 2020-04-05 17:38:52 +0300 |
---|---|---|
committer | Arun Raghavan <arun@arunraghavan.net> | 2020-04-05 22:10:47 +0300 |
commit | dc3c8fd0494056ae5e5f87aa716b4c3866af6591 (patch) | |
tree | 3226cac3439fd29b7b02f6456f3162d6a3d1e00a /generic/sodium | |
parent | 205b6040fbb918c0fa736874b09f8e3f3f261e44 (diff) |
Drop gst-plugin- prefix in plugin directory name
Diffstat (limited to 'generic/sodium')
-rw-r--r-- | generic/sodium/Cargo.toml | 57 | ||||
-rw-r--r-- | generic/sodium/build.rs | 5 | ||||
-rw-r--r-- | generic/sodium/examples/decrypt_example.rs | 150 | ||||
-rw-r--r-- | generic/sodium/examples/encrypt_example.rs | 145 | ||||
-rw-r--r-- | generic/sodium/examples/generate_keys.rs | 102 | ||||
-rw-r--r-- | generic/sodium/examples/receiver_sample.json | 2 | ||||
-rw-r--r-- | generic/sodium/examples/sender_sample.json | 1 | ||||
-rw-r--r-- | generic/sodium/src/decrypter.rs | 733 | ||||
-rw-r--r-- | generic/sodium/src/encrypter.rs | 572 | ||||
-rw-r--r-- | generic/sodium/src/lib.rs | 83 | ||||
-rw-r--r-- | generic/sodium/tests/decrypter.rs | 323 | ||||
-rw-r--r-- | generic/sodium/tests/encrypted_sample.enc | bin | 0 -> 6043 bytes | |||
-rw-r--r-- | generic/sodium/tests/encrypter.rs | 152 | ||||
-rw-r--r-- | generic/sodium/tests/sample.mp3 | bin | 0 -> 5907 bytes |
14 files changed, 2325 insertions, 0 deletions
diff --git a/generic/sodium/Cargo.toml b/generic/sodium/Cargo.toml new file mode 100644 index 000000000..a862ef9f6 --- /dev/null +++ b/generic/sodium/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "gst-plugin-sodium" +version = "0.1.0" +authors = ["Jordan Petridis <jordan@centricular.com>"] +repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugin-rs" +description = "libsodium-based file encryption and decryption" +license = "MIT" +edition = "2018" + +[dependencies] +glib = { git = "https://github.com/gtk-rs/glib" } +gst = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_14"], package="gstreamer" } +gst-base = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_14"], package = "gstreamer-base" } +sodiumoxide = "0.2.1" +lazy_static = "1.3.0" +hex = "0.4" +smallvec = "1.0" + +# example +clap = { version = "2.33", optional = true } +serde = { version = "1.0", features = ["derive"], optional = true } +serde_json = { version = "1.0", optional = true } + +[dev-dependencies] +pretty_assertions = "0.6" +rand = "0.7" + +[dev-dependencies.gst-check] +git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" +package="gstreamer-check" + +[dev-dependencies.gst-app] +git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" +package="gstreamer-app" + +[lib] +name = "gstsodium" +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[[example]] +name = "generate-keys" +path = "examples/generate_keys.rs" +required-features = ["serde", "serde_json", "clap"] + +[[example]] +name = "encrypt-example" +path = "examples/encrypt_example.rs" +required-features = ["serde", "serde_json", "clap"] + +[[example]] +name = "decrypt-example" +path = "examples/decrypt_example.rs" +required-features = ["serde", "serde_json", "clap"] + +[build-dependencies] +gst-plugin-version-helper = { path="../../version-helper" } diff --git a/generic/sodium/build.rs b/generic/sodium/build.rs new file mode 100644 index 000000000..0d1ddb61d --- /dev/null +++ b/generic/sodium/build.rs @@ -0,0 +1,5 @@ +extern crate gst_plugin_version_helper; + +fn main() { + gst_plugin_version_helper::get_info() +} diff --git a/generic/sodium/examples/decrypt_example.rs b/generic/sodium/examples/decrypt_example.rs new file mode 100644 index 000000000..9908a0fd3 --- /dev/null +++ b/generic/sodium/examples/decrypt_example.rs @@ -0,0 +1,150 @@ +// decrypt_example.rs +// +// Copyright 2019 Jordan Petridis <jordan@centricular.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// +// SPDX-License-Identifier: MIT + +use glib::prelude::*; +use gst::prelude::*; +use sodiumoxide::crypto::box_; + +use std::error::Error; +use std::fs::File; +use std::path::PathBuf; + +use clap::{App, Arg}; +use serde::{Deserialize, Serialize}; +use serde_json; + +#[derive(Debug, Serialize, Deserialize)] +struct Keys { + public: box_::PublicKey, + private: box_::SecretKey, +} + +impl Keys { + fn from_file(file: &PathBuf) -> Result<Self, Box<dyn Error>> { + let f = File::open(&file)?; + serde_json::from_reader(f).map_err(From::from) + } +} + +fn main() -> Result<(), Box<dyn Error>> { + let matches = App::new("Decrypt a gstsodium10 file.") + .version("1.0") + .author("Jordan Petridis <jordan@centricular.com>") + .arg( + Arg::with_name("input") + .short("i") + .long("input") + .value_name("FILE") + .help("File to encrypt") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("output") + .short("o") + .long("output") + .value_name("FILE") + .help("File to decrypt") + .required(true) + .takes_value(true), + ) + .get_matches(); + + gst::init()?; + gstsodium::plugin_register_static().expect("Failed to register sodium plugin"); + + let input_loc = matches.value_of("input").unwrap(); + let out_loc = matches.value_of("output").unwrap(); + + let receiver_keys = { + let mut r = PathBuf::new(); + r.push(env!("CARGO_MANIFEST_DIR")); + r.push("examples"); + r.push("receiver_sample"); + r.set_extension("json"); + r + }; + + let sender_keys = { + let mut s = PathBuf::new(); + s.push(env!("CARGO_MANIFEST_DIR")); + s.push("examples"); + s.push("sender_sample"); + s.set_extension("json"); + s + }; + + let receiver = &Keys::from_file(&receiver_keys)?; + let sender = &Keys::from_file(&sender_keys)?; + + let filesrc = gst::ElementFactory::make("filesrc", None).unwrap(); + let decrypter = gst::ElementFactory::make("sodiumdecrypter", None).unwrap(); + let typefind = gst::ElementFactory::make("typefind", None).unwrap(); + let filesink = gst::ElementFactory::make("filesink", None).unwrap(); + + filesrc + .set_property("location", &input_loc) + .expect("Failed to set location property"); + filesink + .set_property("location", &out_loc) + .expect("Failed to set location property"); + + decrypter.set_property("receiver-key", &glib::Bytes::from_owned(receiver.private.0))?; + decrypter.set_property("sender-key", &glib::Bytes::from_owned(sender.public))?; + + let pipeline = gst::Pipeline::new(Some("test-pipeline")); + pipeline + .add_many(&[&filesrc, &decrypter, &typefind, &filesink]) + .expect("failed to add elements to the pipeline"); + gst::Element::link_many(&[&filesrc, &decrypter, &typefind, &filesink]) + .expect("failed to link the elements"); + + pipeline + .set_state(gst::State::Playing) + .expect("Unable to set the pipeline to the `Playing` state"); + + let bus = pipeline.get_bus().unwrap(); + for msg in bus.iter_timed(gst::CLOCK_TIME_NONE) { + use gst::MessageView; + match msg.view() { + MessageView::Error(err) => { + eprintln!( + "Error received from element {:?}: {}", + err.get_src().map(|s| s.get_path_string()), + err.get_error() + ); + eprintln!("Debugging information: {:?}", err.get_debug()); + break; + } + MessageView::Eos(..) => break, + _ => (), + } + } + + pipeline + .set_state(gst::State::Null) + .expect("Unable to set the pipeline to the `Playing` state"); + + Ok(()) +} diff --git a/generic/sodium/examples/encrypt_example.rs b/generic/sodium/examples/encrypt_example.rs new file mode 100644 index 000000000..6189e3242 --- /dev/null +++ b/generic/sodium/examples/encrypt_example.rs @@ -0,0 +1,145 @@ +// encrypt_example.rs +// +// Copyright 2019 Jordan Petridis <jordan@centricular.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// +// SPDX-License-Identifier: MIT + +use glib::prelude::*; +use gst::prelude::*; +use sodiumoxide::crypto::box_; + +use std::error::Error; +use std::fs::File; +use std::path::PathBuf; + +use clap::{App, Arg}; +use serde::{Deserialize, Serialize}; +use serde_json; + +#[derive(Debug, Serialize, Deserialize)] +struct Keys { + public: box_::PublicKey, + private: box_::SecretKey, +} + +impl Keys { + fn from_file(file: &PathBuf) -> Result<Self, Box<dyn Error>> { + let f = File::open(&file)?; + serde_json::from_reader(f).map_err(From::from) + } +} + +fn main() -> Result<(), Box<dyn Error>> { + let matches = App::new("Encrypt a file with in the gstsodium10 format") + .version("1.0") + .author("Jordan Petridis <jordan@centricular.com>") + .arg( + Arg::with_name("input") + .short("i") + .long("input") + .value_name("FILE") + .help("File to encrypt") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("output") + .short("o") + .long("output") + .value_name("FILE") + .help("File to decrypt") + .required(true) + .takes_value(true), + ) + .get_matches(); + + gst::init()?; + gstsodium::plugin_register_static().expect("Failed to register sodium plugin"); + + let input_loc = matches.value_of("input").unwrap(); + let out_loc = matches.value_of("output").unwrap(); + + let receiver_keys = { + let mut r = PathBuf::new(); + r.push(env!("CARGO_MANIFEST_DIR")); + r.push("examples"); + r.push("receiver_sample"); + r.set_extension("json"); + r + }; + + let sender_keys = { + let mut s = PathBuf::new(); + s.push(env!("CARGO_MANIFEST_DIR")); + s.push("examples"); + s.push("sender_sample"); + s.set_extension("json"); + s + }; + + let receiver = &Keys::from_file(&receiver_keys)?; + let sender = &Keys::from_file(&sender_keys)?; + + let filesrc = gst::ElementFactory::make("filesrc", None).unwrap(); + let encrypter = gst::ElementFactory::make("sodiumencrypter", None).unwrap(); + let filesink = gst::ElementFactory::make("filesink", None).unwrap(); + + filesrc + .set_property("location", &input_loc) + .expect("Failed to set location property"); + filesink + .set_property("location", &out_loc) + .expect("Failed to set location property"); + + encrypter.set_property("receiver-key", &glib::Bytes::from_owned(receiver.public))?; + encrypter.set_property("sender-key", &glib::Bytes::from_owned(sender.private.0))?; + + let pipeline = gst::Pipeline::new(Some("test-pipeline")); + pipeline + .add_many(&[&filesrc, &encrypter, &filesink]) + .expect("failed to add elements to the pipeline"); + gst::Element::link_many(&[&filesrc, &encrypter, &filesink]) + .expect("failed to link the elements"); + + pipeline.set_state(gst::State::Playing)?; + + let bus = pipeline.get_bus().unwrap(); + for msg in bus.iter_timed(gst::CLOCK_TIME_NONE) { + use gst::MessageView; + match msg.view() { + MessageView::Error(err) => { + eprintln!( + "Error received from element {:?}: {}", + err.get_src().map(|s| s.get_path_string()), + err.get_error() + ); + eprintln!("Debugging information: {:?}", err.get_debug()); + break; + } + MessageView::Eos(..) => break, + _ => (), + } + } + + pipeline.set_state(gst::State::Null)?; + + Ok(()) +} diff --git a/generic/sodium/examples/generate_keys.rs b/generic/sodium/examples/generate_keys.rs new file mode 100644 index 000000000..b7de47772 --- /dev/null +++ b/generic/sodium/examples/generate_keys.rs @@ -0,0 +1,102 @@ +// generate_keys.rs +// +// Copyright 2019 Jordan Petridis <jordan@centricular.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// +// SPDX-License-Identifier: MIT + +use clap::{App, Arg}; +use serde::{Deserialize, Serialize}; +use serde_json; +use sodiumoxide::crypto::box_; +use std::fs::File; + +#[derive(Debug, Serialize, Deserialize)] +struct Keys { + public: box_::PublicKey, + private: box_::SecretKey, +} + +impl Keys { + fn new() -> Self { + let (public, private) = box_::gen_keypair(); + Keys { public, private } + } + + fn write_to_file(&self, path: &str, json: bool) { + if json { + let path = if !path.ends_with(".json") { + format!("{}.json", path) + } else { + path.into() + }; + + let file = + File::create(&path).unwrap_or_else(|_| panic!("Failed to create file at {}", path)); + serde_json::to_writer(file, &self) + .unwrap_or_else(|_| panic!("Failed to write to file at {}", path)); + } else { + use std::io::Write; + use std::path::PathBuf; + + let mut private = PathBuf::from(path); + private.set_extension("prv"); + let mut file = File::create(&private) + .unwrap_or_else(|_| panic!("Failed to create file at {}", private.display())); + file.write_all(&self.private.0) + .unwrap_or_else(|_| panic!("Failed to write to file at {}", private.display())); + + let mut public = PathBuf::from(path); + public.set_extension("pub"); + let mut file = File::create(&public) + .unwrap_or_else(|_| panic!("Failed to create file at {}", public.display())); + file.write_all(self.public.as_ref()) + .unwrap_or_else(|_| panic!("Failed to write to file at {}", public.display())); + } + } +} + +fn main() { + let matches = App::new("Generate the keys to be used with the sodium element") + .version("1.0") + .author("Jordan Petridis <jordan@centricular.com>") + .about("Generate a pair of Sodium's crypto_box_curve25519xsalsa20poly1305 keys.") + .arg( + Arg::with_name("path") + .long("path") + .short("p") + .value_name("FILE") + .help("Path to write the Keys") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("json") + .long("json") + .short("j") + .help("Write a JSON file instead of a key.prv/key.pub pair"), + ) + .get_matches(); + + let keys = Keys::new(); + + let path = matches.value_of("path").unwrap(); + keys.write_to_file(path, matches.is_present("json")); +} diff --git a/generic/sodium/examples/receiver_sample.json b/generic/sodium/examples/receiver_sample.json new file mode 100644 index 000000000..619b365f8 --- /dev/null +++ b/generic/sodium/examples/receiver_sample.json @@ -0,0 +1,2 @@ + +{"public":[28,95,33,124,28,103,80,78,7,28,234,40,226,179,253,166,169,64,78,5,57,92,151,179,221,89,68,70,44,225,219,19],"private":[54,221,217,54,94,235,167,2,187,249,71,31,59,27,19,166,78,236,102,48,29,142,41,189,22,146,218,69,147,165,240,235]} diff --git a/generic/sodium/examples/sender_sample.json b/generic/sodium/examples/sender_sample.json new file mode 100644 index 000000000..d7ab1fa6c --- /dev/null +++ b/generic/sodium/examples/sender_sample.json @@ -0,0 +1 @@ +{"public":[66,248,199,74,216,55,228,116,52,17,147,56,65,130,134,148,157,153,235,171,179,147,120,71,100,243,133,120,160,14,111,65],"private":[154,227,90,239,206,184,202,234,176,161,14,91,218,98,142,13,145,223,210,222,224,240,98,51,142,165,255,1,159,100,242,162]} diff --git a/generic/sodium/src/decrypter.rs b/generic/sodium/src/decrypter.rs new file mode 100644 index 000000000..39891ee17 --- /dev/null +++ b/generic/sodium/src/decrypter.rs @@ -0,0 +1,733 @@ +// decrypter.rs +// +// Copyright 2019 Jordan Petridis <jordan@centricular.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// +// SPDX-License-Identifier: MIT + +use glib::prelude::*; +use glib::subclass; +use glib::subclass::prelude::*; +use gst::prelude::*; +use gst::subclass::prelude::*; +use sodiumoxide::crypto::box_; + +use std::sync::Mutex; + +lazy_static! { + static ref CAT: gst::DebugCategory = { + gst::DebugCategory::new( + "sodiumdecrypter", + gst::DebugColorFlags::empty(), + Some("Decrypter Element"), + ) + }; +} + +static PROPERTIES: [subclass::Property; 2] = [ + subclass::Property("receiver-key", |name| { + glib::ParamSpec::boxed( + name, + "Receiver Key", + "The private key of the Reeiver", + glib::Bytes::static_type(), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("sender-key", |name| { + glib::ParamSpec::boxed( + name, + "Sender Key", + "The public key of the Sender", + glib::Bytes::static_type(), + glib::ParamFlags::WRITABLE, + ) + }), +]; + +#[derive(Debug, Clone, Default)] +struct Props { + receiver_key: Option<glib::Bytes>, + sender_key: Option<glib::Bytes>, +} + +#[derive(Debug)] +struct State { + adapter: gst_base::UniqueAdapter, + initial_nonce: Option<box_::Nonce>, + precomputed_key: box_::PrecomputedKey, + block_size: Option<u32>, +} + +impl State { + fn from_props(props: &Props) -> Result<Self, gst::ErrorMessage> { + let sender_key = props + .sender_key + .as_ref() + .and_then(|k| box_::PublicKey::from_slice(&k)) + .ok_or_else(|| { + gst_error_msg!( + gst::ResourceError::NotFound, + [format!( + "Failed to set Sender's Key from property: {:?}", + props.sender_key + ) + .as_ref()] + ) + })?; + + let receiver_key = props + .receiver_key + .as_ref() + .and_then(|k| box_::SecretKey::from_slice(&k)) + .ok_or_else(|| { + gst_error_msg!( + gst::ResourceError::NotFound, + [format!( + "Failed to set Receiver's Key from property: {:?}", + props.receiver_key + ) + .as_ref()] + ) + })?; + + let precomputed_key = box_::precompute(&sender_key, &receiver_key); + + Ok(Self { + adapter: gst_base::UniqueAdapter::new(), + precomputed_key, + initial_nonce: None, + block_size: None, + }) + } + + // Split the buffer into N(`chunk_index`) chunks of `block_size`, + // decrypt them, and push them to the internal adapter for further + // retrieval + fn decrypt_into_adapter( + &mut self, + element: &gst::Element, + pad: &gst::Pad, + buffer: &gst::Buffer, + chunk_index: u64, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + let map = buffer.map_readable().map_err(|_| { + gst_element_error!( + element, + gst::StreamError::Format, + ["Failed to map buffer readable"] + ); + + gst::FlowError::Error + })?; + + gst_debug!(CAT, obj: pad, "Returned pull size: {}", map.len()); + + let mut nonce = add_nonce(self.initial_nonce.unwrap(), chunk_index); + let block_size = self.block_size.expect("Block size wasn't set") as usize + box_::MACBYTES; + + for subbuffer in map.chunks(block_size) { + let plain = box_::open_precomputed(&subbuffer, &nonce, &self.precomputed_key).map_err( + |_| { + gst_element_error!( + element, + gst::StreamError::Format, + ["Failed to decrypt buffer"] + ); + gst::FlowError::Error + }, + )?; + // assumes little endian + nonce.increment_le_inplace(); + self.adapter.push(gst::Buffer::from_mut_slice(plain)); + } + + Ok(gst::FlowSuccess::Ok) + } + + // Retrieve the requested buffer out of the adapter. + fn get_requested_buffer( + &mut self, + pad: &gst::Pad, + buffer: Option<&mut gst::BufferRef>, + requested_size: u32, + adapter_offset: usize, + ) -> Result<gst::PadGetRangeSuccess, gst::FlowError> { + let avail = self.adapter.available(); + gst_debug!(CAT, obj: pad, "Avail: {}", avail); + gst_debug!(CAT, obj: pad, "Adapter offset: {}", adapter_offset); + + // if this underflows, the available buffer in the adapter is smaller than the + // requested offset, which means we have reached EOS + let available_buffer = avail + .checked_sub(adapter_offset) + .ok_or(gst::FlowError::Eos)?; + + // if the available buffer size is smaller than the requested, it's a short + // read and return that. Else return the requested size + let available_size = if available_buffer <= requested_size as usize { + available_buffer + } else { + requested_size as usize + }; + + if available_size == 0 { + self.adapter.clear(); + + // if the requested buffer was 0 sized, return an + // empty buffer + if requested_size == 0 { + if let Some(buffer) = buffer { + buffer.set_size(0); + return Ok(gst::PadGetRangeSuccess::FilledBuffer); + } else { + return Ok(gst::PadGetRangeSuccess::NewBuffer(gst::Buffer::new())); + } + } + + return Err(gst::FlowError::Eos); + } + + // discard what we don't need + assert!(self.adapter.available() >= adapter_offset); + self.adapter.flush(adapter_offset); + + assert!(self.adapter.available() >= available_size); + let res = if let Some(buffer) = buffer { + let mut map = match buffer.map_writable() { + Ok(map) => map, + Err(_) => { + gst_error!(CAT, obj: pad, "Failed to map provided buffer writable"); + return Err(gst::FlowError::Error); + } + }; + self.adapter.copy(0, &mut map[..available_size]); + if map.len() != available_size { + drop(map); + buffer.set_size(available_size); + } + gst::PadGetRangeSuccess::FilledBuffer + } else { + let buffer = self + .adapter + .take_buffer(available_size) + .expect("Failed to get buffer from adapter"); + gst::PadGetRangeSuccess::NewBuffer(buffer) + }; + + // Cleanup the adapter + self.adapter.clear(); + + Ok(res) + } +} + +/// Calculate the nonce of a block based on the initial nonce +/// and the block index in the stream. +/// +/// This is a faster way of doing `(0..chunk_index).for_each(|_| nonce.increment_le_inplace());` +fn add_nonce(initial_nonce: box_::Nonce, chunk_index: u64) -> box_::Nonce { + let mut nonce = initial_nonce.0; + // convert our index to a bytes array + // add padding so our 8byte array of the chunk_index will have an + // equal length with the nonce, padding at the end cause little endian + let idx = chunk_index.to_le_bytes(); + let idx = &[ + idx[0], idx[1], idx[2], idx[3], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + assert_eq!(idx.len(), box_::NONCEBYTES); + + // add the chunk index to the nonce + sodiumoxide::utils::add_le(&mut nonce, idx).expect("Failed to calculate the nonce"); + + // construct back a nonce from our custom array + box_::Nonce::from_slice(&nonce).expect("Failed to convert slice back to Nonce") +} + +struct Decrypter { + srcpad: gst::Pad, + sinkpad: gst::Pad, + props: Mutex<Props>, + state: Mutex<Option<State>>, +} + +impl Decrypter { + fn set_pad_functions(_sinkpad: &gst::Pad, srcpad: &gst::Pad) { + srcpad.set_getrange_function(|pad, parent, offset, buffer, size| { + Decrypter::catch_panic_pad_function( + parent, + || Err(gst::FlowError::Error), + |decrypter, element| decrypter.get_range(pad, element, offset, buffer, size), + ) + }); + + srcpad.set_activatemode_function(|pad, parent, mode, active| { + Decrypter::catch_panic_pad_function( + parent, + || { + Err(gst_loggable_error!( + CAT, + "Panic activating srcpad with mode" + )) + }, + |decrypter, element| { + decrypter.src_activatemode_function(pad, element, mode, active) + }, + ) + }); + + srcpad.set_query_function(|pad, parent, query| { + Decrypter::catch_panic_pad_function( + parent, + || false, + |decrypter, element| decrypter.src_query(pad, element, query), + ) + }); + } + + fn src_activatemode_function( + &self, + _pad: &gst::Pad, + element: &gst::Element, + mode: gst::PadMode, + active: bool, + ) -> Result<(), gst::LoggableError> { + match mode { + gst::PadMode::Pull => { + self.sinkpad + .activate_mode(mode, active) + .map_err(gst::LoggableError::from)?; + + // Set the nonce and block size from the headers + // right after we activate the pad + self.check_headers(element) + } + gst::PadMode::Push => Err(gst_loggable_error!(CAT, "Push mode not supported")), + _ => Err(gst_loggable_error!( + CAT, + "Failed to activate the pad in Unknown mode, {:?}", + mode + )), + } + } + + fn src_query(&self, pad: &gst::Pad, element: &gst::Element, query: &mut gst::QueryRef) -> bool { + use gst::QueryView; + + gst_log!(CAT, obj: pad, "Handling query {:?}", query); + + match query.view_mut() { + QueryView::Scheduling(mut q) => { + let mut peer_query = gst::Query::new_scheduling(); + let res = self.sinkpad.peer_query(&mut peer_query); + if !res { + return res; + } + + gst_log!(CAT, obj: pad, "Upstream returned {:?}", peer_query); + + let (flags, min, max, align) = peer_query.get_result(); + q.set(flags, min, max, align); + q.add_scheduling_modes(&[gst::PadMode::Pull]); + gst_log!(CAT, obj: pad, "Returning {:?}", q.get_mut_query()); + true + } + QueryView::Duration(ref mut q) => { + use std::convert::TryInto; + + if q.get_format() != gst::Format::Bytes { + return pad.query_default(Some(element), query); + } + + /* First let's query the bytes duration upstream */ + let mut peer_query = gst::query::Query::new_duration(gst::Format::Bytes); + + if !self.sinkpad.peer_query(&mut peer_query) { + gst_error!(CAT, "Failed to query upstream duration"); + return false; + } + + let size = match peer_query.get_result().try_into().unwrap() { + gst::format::Bytes(Some(size)) => size, + gst::format::Bytes(None) => { + gst_error!(CAT, "Failed to query upstream duration"); + + return false; + } + }; + + let state = self.state.lock().unwrap(); + let state = match state.as_ref() { + // If state isn't set, it means that the + // element hasn't been activated yet. + None => return false, + Some(s) => s, + }; + + // subtract static offsets + let size = size - super::HEADERS_SIZE as u64; + + // calculate the number of chunks that exist in the stream + let total_chunks = + (size - 1) / state.block_size.expect("Block size wasn't set") as u64; + // subtrack the MAC of each block + let size = size - total_chunks * box_::MACBYTES as u64; + + gst_debug!(CAT, obj: pad, "Setting duration bytes: {}", size); + q.set(gst::format::Bytes::from(size)); + + true + } + _ => pad.query_default(Some(element), query), + } + } + + fn check_headers(&self, element: &gst::Element) -> Result<(), gst::LoggableError> { + let is_none = { + let mutex_state = self.state.lock().unwrap(); + let state = mutex_state.as_ref().unwrap(); + state.initial_nonce.is_none() + }; + + if !is_none { + return Ok(()); + } + + let buffer = self + .sinkpad + .pull_range(0, crate::HEADERS_SIZE as u32) + .map_err(|err| { + let err = gst_loggable_error!( + CAT, + "Failed to pull nonce from the stream, reason: {:?}", + err + ); + err.log_with_object(element); + err + })?; + + if buffer.get_size() != crate::HEADERS_SIZE { + let err = gst_loggable_error!(CAT, "Headers buffer has wrong size"); + err.log_with_object(element); + return Err(err); + } + + let map = buffer.map_readable().map_err(|_| { + let err = gst_loggable_error!(CAT, "Failed to map buffer readable"); + err.log_with_object(element); + err + })?; + + let sodium_header_slice = &map[..crate::TYPEFIND_HEADER_SIZE]; + if sodium_header_slice != crate::TYPEFIND_HEADER { + let err = gst_loggable_error!(CAT, "Buffer has wrong typefind header"); + err.log_with_object(element); + return Err(err); + } + + let nonce_slice = + &map[crate::TYPEFIND_HEADER_SIZE..crate::TYPEFIND_HEADER_SIZE + box_::NONCEBYTES]; + assert_eq!(nonce_slice.len(), box_::NONCEBYTES); + let nonce = box_::Nonce::from_slice(nonce_slice).ok_or_else(|| { + let err = gst_loggable_error!(CAT, "Failed to create nonce from buffer"); + err.log_with_object(&self.srcpad); + err + })?; + + let slice = &map[crate::TYPEFIND_HEADER_SIZE + box_::NONCEBYTES..crate::HEADERS_SIZE]; + assert_eq!( + crate::HEADERS_SIZE - crate::TYPEFIND_HEADER_SIZE - box_::NONCEBYTES, + 4 + ); + let block_size = u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]); + + // reacquire the lock again to change the state + let mut state = self.state.lock().unwrap(); + let state = state.as_mut().unwrap(); + + state.initial_nonce = Some(nonce); + gst_debug!(CAT, obj: element, "Setting nonce to: {:?}", nonce.0); + state.block_size = Some(block_size); + gst_debug!(CAT, obj: element, "Setting block size to: {}", block_size); + + Ok(()) + } + + fn pull_requested_buffer( + &self, + pad: &gst::Pad, + element: &gst::Element, + requested_size: u32, + block_size: u32, + chunk_index: u64, + ) -> Result<gst::Buffer, gst::FlowError> { + let pull_offset = super::HEADERS_SIZE as u64 + + (chunk_index * block_size as u64) + + (chunk_index * box_::MACBYTES as u64); + + gst_debug!(CAT, obj: pad, "Pull offset: {}", pull_offset); + gst_debug!(CAT, obj: pad, "block size: {}", block_size); + + // calculate how many chunks are needed, if we need something like 3.2 + // round the number to 4 and cut the buffer afterwards. + let checked = requested_size.checked_add(block_size).ok_or_else(|| { + gst_element_error!( + element, + gst::LibraryError::Failed, + [ + "Addition overflow when adding requested pull size and block size: {} + {}", + requested_size, + block_size, + ] + ); + gst::FlowError::Error + })?; + + // Read at least one chunk in case 0 bytes were requested + let total_chunks = u32::max((checked - 1) / block_size, 1); + gst_debug!(CAT, obj: pad, "Blocks to be pulled: {}", total_chunks); + + // Pull a buffer of all the chunks we will need + let checked_size = total_chunks.checked_mul(block_size).ok_or_else(|| { + gst_element_error!( + element, + gst::LibraryError::Failed, + [ + "Overflowed trying to calculate the buffer size to pull: {} * {}", + total_chunks, + block_size, + ] + ); + gst::FlowError::Error + })?; + + let total_size = checked_size + (total_chunks * box_::MACBYTES as u32); + gst_debug!(CAT, obj: pad, "Requested pull size: {}", total_size); + + self.sinkpad.pull_range(pull_offset, total_size).map_err(|err| { + match err { + gst::FlowError::Flushing => { + gst_debug!(CAT, obj: &self.sinkpad, "Pausing after pulling buffer, reason: flushing"); + } + gst::FlowError::Eos => { + gst_debug!(CAT, obj: &self.sinkpad, "Eos"); + } + flow => { + gst_error!(CAT, obj: &self.sinkpad, "Failed to pull, reason: {:?}", flow); + } + }; + + err + }) + } + + fn get_range( + &self, + pad: &gst::Pad, + element: &gst::Element, + offset: u64, + buffer: Option<&mut gst::BufferRef>, + requested_size: u32, + ) -> Result<gst::PadGetRangeSuccess, gst::FlowError> { + let block_size = { + let mut mutex_state = self.state.lock().unwrap(); + // This will only be run after READY state, + // and will be guaranted to be initialized + let state = mutex_state.as_mut().unwrap(); + // Cleanup the adapter + state.adapter.clear(); + state.block_size.expect("Block size wasn't set") + }; + + gst_debug!(CAT, obj: pad, "Requested offset: {}", offset); + gst_debug!(CAT, obj: pad, "Requested size: {}", requested_size); + + let chunk_index = offset as u64 / block_size as u64; + gst_debug!(CAT, obj: pad, "Stream Block index: {}", chunk_index); + + let pull_offset = offset - (chunk_index * block_size as u64); + assert!(pull_offset <= std::u32::MAX as u64); + let pull_offset = pull_offset as u32; + + let pulled_buffer = self.pull_requested_buffer( + pad, + element, + requested_size + pull_offset, + block_size, + chunk_index, + )?; + + let mut state = self.state.lock().unwrap(); + // This will only be run after READY state, + // and will be guaranted to be initialized + let state = state.as_mut().unwrap(); + + state.decrypt_into_adapter(element, &self.srcpad, &pulled_buffer, chunk_index)?; + + let adapter_offset = pull_offset as usize; + state.get_requested_buffer(&self.srcpad, buffer, requested_size, adapter_offset) + } +} + +impl ObjectSubclass for Decrypter { + const NAME: &'static str = "RsSodiumDecryptor"; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn new_with_class(klass: &subclass::simple::ClassStruct<Self>) -> Self { + let templ = klass.get_pad_template("sink").unwrap(); + let sinkpad = gst::Pad::new_from_template(&templ, Some("sink")); + let templ = klass.get_pad_template("src").unwrap(); + let srcpad = gst::Pad::new_from_template(&templ, Some("src")); + + Decrypter::set_pad_functions(&sinkpad, &srcpad); + let props = Mutex::new(Props::default()); + let state = Mutex::new(None); + + Self { + srcpad, + sinkpad, + props, + state, + } + } + + fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "Decrypter", + "Generic", + "libsodium-based file decrypter", + "Jordan Petridis <jordan@centricular.com>", + ); + + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &gst::Caps::new_any(), + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + + let sink_caps = gst::Caps::builder("application/x-sodium-encrypted").build(); + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &sink_caps, + ) + .unwrap(); + klass.add_pad_template(sink_pad_template); + klass.install_properties(&PROPERTIES); + } +} + +impl ObjectImpl for Decrypter { + glib_object_impl!(); + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::<gst::Element>().unwrap(); + element.add_pad(&self.sinkpad).unwrap(); + element.add_pad(&self.srcpad).unwrap(); + } + + fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &PROPERTIES[id]; + + match *prop { + subclass::Property("sender-key", ..) => { + let mut props = self.props.lock().unwrap(); + props.sender_key = value.get().expect("type checked upstream"); + } + + subclass::Property("receiver-key", ..) => { + let mut props = self.props.lock().unwrap(); + props.receiver_key = value.get().expect("type checked upstream"); + } + + _ => unimplemented!(), + } + } + + fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> { + let prop = &PROPERTIES[id]; + + match *prop { + subclass::Property("receiver-key", ..) => { + let props = self.props.lock().unwrap(); + Ok(props.receiver_key.to_value()) + } + + _ => unimplemented!(), + } + } +} + +impl ElementImpl for Decrypter { + fn change_state( + &self, + element: &gst::Element, + transition: gst::StateChange, + ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> { + gst_debug!(CAT, obj: element, "Changing state {:?}", transition); + + match transition { + gst::StateChange::NullToReady => { + let props = self.props.lock().unwrap().clone(); + + // Create an internal state struct from the provided properties or + // refuse to change state + let state_ = State::from_props(&props).map_err(|err| { + element.post_error_message(&err); + gst::StateChangeError + })?; + + let mut state = self.state.lock().unwrap(); + *state = Some(state_); + } + gst::StateChange::ReadyToNull => { + let _ = self.state.lock().unwrap().take(); + } + _ => (), + } + + let success = self.parent_change_state(element, transition)?; + + if transition == gst::StateChange::ReadyToNull { + let _ = self.state.lock().unwrap().take(); + } + + Ok(success) + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "sodiumdecrypter", + gst::Rank::None, + Decrypter::get_type(), + ) +} diff --git a/generic/sodium/src/encrypter.rs b/generic/sodium/src/encrypter.rs new file mode 100644 index 000000000..60a6e01e8 --- /dev/null +++ b/generic/sodium/src/encrypter.rs @@ -0,0 +1,572 @@ +// encrypter.rs +// +// Copyright 2019 Jordan Petridis <jordan@centricular.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// +// SPDX-License-Identifier: MIT + +use glib::prelude::*; +use glib::subclass; +use glib::subclass::prelude::*; +use gst::prelude::*; +use gst::subclass::prelude::*; +use smallvec::SmallVec; +use sodiumoxide::crypto::box_; + +type BufferVec = SmallVec<[gst::Buffer; 16]>; + +use std::sync::Mutex; + +lazy_static! { + static ref CAT: gst::DebugCategory = { + gst::DebugCategory::new( + "sodiumencrypter", + gst::DebugColorFlags::empty(), + Some("Encrypter Element"), + ) + }; +} + +static PROPERTIES: [subclass::Property; 3] = [ + subclass::Property("receiver-key", |name| { + glib::ParamSpec::boxed( + name, + "Receiver Key", + "The public key of the Receiver", + glib::Bytes::static_type(), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("sender-key", |name| { + glib::ParamSpec::boxed( + name, + "Sender Key", + "The private key of the Sender", + glib::Bytes::static_type(), + glib::ParamFlags::WRITABLE, + ) + }), + subclass::Property("block-size", |name| { + glib::ParamSpec::uint( + name, + "Block Size", + "The block-size of the chunks", + 1024, + std::u32::MAX, + 32768, + glib::ParamFlags::READWRITE, + ) + }), +]; + +#[derive(Debug, Clone)] +struct Props { + receiver_key: Option<glib::Bytes>, + sender_key: Option<glib::Bytes>, + block_size: u32, +} + +impl Default for Props { + fn default() -> Self { + Props { + receiver_key: None, + sender_key: None, + block_size: 32768, + } + } +} + +#[derive(Debug)] +struct State { + adapter: gst_base::UniqueAdapter, + nonce: box_::Nonce, + precomputed_key: box_::PrecomputedKey, + block_size: u32, + write_headers: bool, +} + +impl State { + fn from_props(props: &Props) -> Result<Self, gst::ErrorMessage> { + let sender_key = props + .sender_key + .as_ref() + .and_then(|k| box_::SecretKey::from_slice(&k)) + .ok_or_else(|| { + gst_error_msg!( + gst::ResourceError::NotFound, + [format!( + "Failed to set Sender's Key from property: {:?}", + props.sender_key + ) + .as_ref()] + ) + })?; + + let receiver_key = props + .receiver_key + .as_ref() + .and_then(|k| box_::PublicKey::from_slice(&k)) + .ok_or_else(|| { + gst_error_msg!( + gst::ResourceError::NotFound, + [format!( + "Failed to set Receiver's Key from property: {:?}", + props.receiver_key + ) + .as_ref()] + ) + })?; + + // This env variable is only meant to bypass nonce regeneration during + // tests to get determinisic results. It should never be used outside + // of testing environments. + let nonce = if let Ok(val) = std::env::var("GST_SODIUM_ENCRYPT_NONCE") { + let bytes = hex::decode(val).expect("Failed to decode hex variable"); + assert_eq!(bytes.len(), box_::NONCEBYTES); + box_::Nonce::from_slice(&bytes).unwrap() + } else { + box_::gen_nonce() + }; + + let precomputed_key = box_::precompute(&receiver_key, &sender_key); + + Ok(Self { + adapter: gst_base::UniqueAdapter::new(), + precomputed_key, + nonce, + block_size: props.block_size, + write_headers: true, + }) + } + + fn seal(&mut self, message: &[u8]) -> Vec<u8> { + let ciphertext = box_::seal_precomputed(message, &self.nonce, &self.precomputed_key); + self.nonce.increment_le_inplace(); + ciphertext + } + + fn encrypt_message(&mut self, buffer: &gst::BufferRef) -> gst::Buffer { + let map = buffer + .map_readable() + .expect("Failed to map buffer readable"); + + let sealed = self.seal(&map); + gst::Buffer::from_mut_slice(sealed) + } + + fn encrypt_blocks(&mut self, block_size: usize) -> Result<BufferVec, gst::FlowError> { + assert_ne!(block_size, 0); + + let mut buffers = BufferVec::new(); + + // As long we have enough bytes to encrypt a block, or more, we do so + // else the leftover bytes on the adapter will be pushed when EOS + // is sent. + while self.adapter.available() >= block_size { + let buffer = self.adapter.take_buffer(block_size).unwrap(); + let out_buf = self.encrypt_message(&buffer); + + buffers.push(out_buf); + } + + Ok(buffers) + } +} + +struct Encrypter { + srcpad: gst::Pad, + sinkpad: gst::Pad, + props: Mutex<Props>, + state: Mutex<Option<State>>, +} + +impl Encrypter { + fn set_pad_functions(sinkpad: &gst::Pad, srcpad: &gst::Pad) { + sinkpad.set_chain_function(|pad, parent, buffer| { + Encrypter::catch_panic_pad_function( + parent, + || Err(gst::FlowError::Error), + |encrypter, element| encrypter.sink_chain(pad, element, buffer), + ) + }); + sinkpad.set_event_function(|pad, parent, event| { + Encrypter::catch_panic_pad_function( + parent, + || false, + |encrypter, element| encrypter.sink_event(pad, element, event), + ) + }); + + srcpad.set_query_function(|pad, parent, query| { + Encrypter::catch_panic_pad_function( + parent, + || false, + |encrypter, element| encrypter.src_query(pad, element, query), + ) + }); + srcpad.set_event_function(|pad, parent, event| { + Encrypter::catch_panic_pad_function( + parent, + || false, + |encrypter, element| encrypter.src_event(pad, element, event), + ) + }); + } + + fn sink_chain( + &self, + pad: &gst::Pad, + element: &gst::Element, + buffer: gst::Buffer, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + gst_log!(CAT, obj: pad, "Handling buffer {:?}", buffer); + + let mut buffers = BufferVec::new(); + let mut state_guard = self.state.lock().unwrap(); + let state = state_guard.as_mut().unwrap(); + + if state.write_headers { + let mut headers = Vec::with_capacity(40); + headers.extend_from_slice(crate::TYPEFIND_HEADER); + // Write the Nonce used into the stream. + headers.extend_from_slice(state.nonce.as_ref()); + // Write the block_size into the stream + headers.extend_from_slice(&state.block_size.to_le_bytes()); + + buffers.push(gst::Buffer::from_mut_slice(headers)); + state.write_headers = false; + } + + state.adapter.push(buffer); + + // Encrypt the whole blocks, if any, and push them. + buffers.extend( + state + .encrypt_blocks(state.block_size as usize) + .map_err(|err| { + // log the error to the bus + gst_element_error!( + element, + gst::ResourceError::Write, + ["Failed to decrypt buffer"] + ); + err + })?, + ); + + drop(state_guard); + + for buffer in buffers { + self.srcpad.push(buffer).map_err(|err| { + gst_error!(CAT, obj: element, "Failed to push buffer {:?}", err); + err + })?; + } + + Ok(gst::FlowSuccess::Ok) + } + + fn sink_event(&self, pad: &gst::Pad, element: &gst::Element, event: gst::Event) -> bool { + use gst::EventView; + + gst_log!(CAT, obj: pad, "Handling event {:?}", event); + + match event.view() { + EventView::Caps(_) => { + // We send our own caps downstream + let caps = gst::Caps::builder("application/x-sodium-encrypted").build(); + self.srcpad.push_event(gst::Event::new_caps(&caps).build()) + } + EventView::Eos(_) => { + let mut state_mutex = self.state.lock().unwrap(); + let mut buffers = BufferVec::new(); + // This will only be run after READY state, + // and will be guaranted to be initialized + let state = state_mutex.as_mut().unwrap(); + + // Now that all the full size blocks are pushed, drain the + // rest of the adapter and push whatever is left. + let avail = state.adapter.available(); + // logic error, all the complete blocks that can be pushed + // should have been done in the sink_chain call. + assert!(avail < state.block_size as usize); + + if avail > 0 { + match state.encrypt_blocks(avail) { + Err(_) => { + gst_element_error!( + element, + gst::ResourceError::Write, + ["Failed to encrypt buffers at EOS"] + ); + return false; + } + Ok(b) => buffers.extend(b), + } + } + + // drop the lock before pushing into the pad + drop(state_mutex); + + for buffer in buffers { + if let Err(err) = self.srcpad.push(buffer) { + gst_error!(CAT, obj: element, "Failed to push buffer at EOS {:?}", err); + return false; + } + } + + pad.event_default(Some(element), event) + } + _ => pad.event_default(Some(element), event), + } + } + + fn src_event(&self, pad: &gst::Pad, element: &gst::Element, event: gst::Event) -> bool { + use gst::EventView; + + gst_log!(CAT, obj: pad, "Handling event {:?}", event); + + match event.view() { + EventView::Seek(_) => false, + _ => pad.event_default(Some(element), event), + } + } + + fn src_query(&self, pad: &gst::Pad, element: &gst::Element, query: &mut gst::QueryRef) -> bool { + use gst::QueryView; + + gst_log!(CAT, obj: pad, "Handling query {:?}", query); + + match query.view_mut() { + QueryView::Seeking(mut q) => { + let format = q.get_format(); + q.set( + false, + gst::GenericFormattedValue::Other(format, -1), + gst::GenericFormattedValue::Other(format, -1), + ); + gst_log!(CAT, obj: pad, "Returning {:?}", q.get_mut_query()); + true + } + QueryView::Duration(ref mut q) => { + use std::convert::TryInto; + + if q.get_format() != gst::Format::Bytes { + return pad.query_default(Some(element), query); + } + + /* First let's query the bytes duration upstream */ + let mut peer_query = gst::query::Query::new_duration(gst::Format::Bytes); + + if !self.sinkpad.peer_query(&mut peer_query) { + gst_error!(CAT, "Failed to query upstream duration"); + return false; + } + + let size = match peer_query.get_result().try_into().unwrap() { + gst::format::Bytes(Some(size)) => size, + gst::format::Bytes(None) => { + gst_error!(CAT, "Failed to query upstream duration"); + + return false; + } + }; + + let state = self.state.lock().unwrap(); + let state = match state.as_ref() { + // If state isn't set, it means that the + // element hasn't been activated yet. + None => return false, + Some(s) => s, + }; + + // calculate the number of chunks that exist in the stream + let total_chunks = (size + state.block_size as u64 - 1) / state.block_size as u64; + // add the MAC of each block + let size = size + total_chunks * box_::MACBYTES as u64; + + // add static offsets + let size = size + super::HEADERS_SIZE as u64; + + gst_debug!(CAT, obj: pad, "Setting duration bytes: {}", size); + q.set(gst::format::Bytes::from(size)); + + true + } + _ => pad.query_default(Some(element), query), + } + } +} + +impl ObjectSubclass for Encrypter { + const NAME: &'static str = "RsSodiumEncrypter"; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn new_with_class(klass: &subclass::simple::ClassStruct<Self>) -> Self { + let templ = klass.get_pad_template("sink").unwrap(); + let sinkpad = gst::Pad::new_from_template(&templ, Some("sink")); + let templ = klass.get_pad_template("src").unwrap(); + let srcpad = gst::Pad::new_from_template(&templ, Some("src")); + + Encrypter::set_pad_functions(&sinkpad, &srcpad); + let props = Mutex::new(Props::default()); + let state = Mutex::new(None); + + Self { + srcpad, + sinkpad, + props, + state, + } + } + + fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "Encrypter", + "Generic", + "libsodium-based file encrypter", + "Jordan Petridis <jordan@centricular.com>", + ); + + let src_caps = gst::Caps::builder("application/x-sodium-encrypted").build(); + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &src_caps, + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &gst::Caps::new_any(), + ) + .unwrap(); + klass.add_pad_template(sink_pad_template); + klass.install_properties(&PROPERTIES); + } +} + +impl ObjectImpl for Encrypter { + glib_object_impl!(); + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::<gst::Element>().unwrap(); + element.add_pad(&self.sinkpad).unwrap(); + element.add_pad(&self.srcpad).unwrap(); + } + + fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &PROPERTIES[id]; + + match *prop { + subclass::Property("sender-key", ..) => { + let mut props = self.props.lock().unwrap(); + props.sender_key = value.get().expect("type checked upstream"); + } + + subclass::Property("receiver-key", ..) => { + let mut props = self.props.lock().unwrap(); + props.receiver_key = value.get().expect("type checked upstream"); + } + + subclass::Property("block-size", ..) => { + let mut props = self.props.lock().unwrap(); + props.block_size = value.get_some().expect("type checked upstream"); + } + + _ => unimplemented!(), + } + } + + fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> { + let prop = &PROPERTIES[id]; + + match *prop { + subclass::Property("receiver-key", ..) => { + let props = self.props.lock().unwrap(); + Ok(props.receiver_key.to_value()) + } + + subclass::Property("block-size", ..) => { + let props = self.props.lock().unwrap(); + Ok(props.block_size.to_value()) + } + + _ => unimplemented!(), + } + } +} + +impl ElementImpl for Encrypter { + fn change_state( + &self, + element: &gst::Element, + transition: gst::StateChange, + ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> { + gst_debug!(CAT, obj: element, "Changing state {:?}", transition); + + match transition { + gst::StateChange::NullToReady => { + let props = self.props.lock().unwrap().clone(); + + // Create an internal state struct from the provided properties or + // refuse to change state + let state_ = State::from_props(&props).map_err(|err| { + element.post_error_message(&err); + gst::StateChangeError + })?; + + let mut state = self.state.lock().unwrap(); + *state = Some(state_); + } + gst::StateChange::ReadyToNull => { + let _ = self.state.lock().unwrap().take(); + } + _ => (), + } + + let success = self.parent_change_state(element, transition)?; + + if transition == gst::StateChange::ReadyToNull { + let _ = self.state.lock().unwrap().take(); + } + + Ok(success) + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "sodiumencrypter", + gst::Rank::None, + Encrypter::get_type(), + ) +} diff --git a/generic/sodium/src/lib.rs b/generic/sodium/src/lib.rs new file mode 100644 index 000000000..3961ddc7c --- /dev/null +++ b/generic/sodium/src/lib.rs @@ -0,0 +1,83 @@ +// Copyright 2019 Jordan Petridis <jordan@centricular.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// +// SPDX-License-Identifier: MIT + +#![crate_type = "cdylib"] + +#[macro_use] +extern crate glib; +#[macro_use] +extern crate gst; +#[macro_use] +extern crate lazy_static; + +const TYPEFIND_HEADER: &[u8; 12] = b"gst-sodium10"; +// `core::slice::<impl [T]>::len` is not yet stable as a const fn +// const TYPEFIND_HEADER_SIZE: usize = TYPEFIND_HEADER.len(); +const TYPEFIND_HEADER_SIZE: usize = 12; +/// Encryted steams use an offset at the start to store the block_size +/// and the nonce that was used. +const HEADERS_SIZE: usize = + TYPEFIND_HEADER_SIZE + sodiumoxide::crypto::box_::NONCEBYTES + std::mem::size_of::<u32>(); + +mod decrypter; +mod encrypter; + +fn typefind_register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + use gst::{Caps, TypeFind, TypeFindProbability}; + + TypeFind::register( + Some(plugin), + "sodium_encrypted_typefind", + gst::Rank::None, + None, + Some(&Caps::new_simple("application/x-sodium-encrypted", &[])), + |typefind| { + if let Some(data) = typefind.peek(0, TYPEFIND_HEADER_SIZE as u32) { + if data == TYPEFIND_HEADER { + typefind.suggest( + TypeFindProbability::Maximum, + &Caps::new_simple("application/x-sodium-encrypted", &[]), + ); + } + } + }, + ) +} + +fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + encrypter::register(plugin)?; + decrypter::register(plugin)?; + typefind_register(plugin)?; + Ok(()) +} + +gst_plugin_define!( + sodium, + env!("CARGO_PKG_DESCRIPTION"), + plugin_init, + concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")), + "MIT/X11", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_REPOSITORY"), + env!("BUILD_REL_DATE") +); diff --git a/generic/sodium/tests/decrypter.rs b/generic/sodium/tests/decrypter.rs new file mode 100644 index 000000000..2fbb91e7a --- /dev/null +++ b/generic/sodium/tests/decrypter.rs @@ -0,0 +1,323 @@ +// decrypter.rs +// +// Copyright 2019 Jordan Petridis <jordan@centricular.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// +// SPDX-License-Identifier: MIT + +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate pretty_assertions; + +extern crate gstsodium; + +use glib::prelude::*; +use gst::prelude::*; + +use std::sync::{Arc, Mutex}; + +use std::path::PathBuf; + +lazy_static! { + static ref SENDER_PUBLIC: glib::Bytes = { + let public = [ + 66, 248, 199, 74, 216, 55, 228, 116, 52, 17, 147, 56, 65, 130, 134, 148, 157, 153, 235, + 171, 179, 147, 120, 71, 100, 243, 133, 120, 160, 14, 111, 65, + ]; + glib::Bytes::from_owned(public) + }; + static ref RECEIVER_PRIVATE: glib::Bytes = { + let secret = [ + 54, 221, 217, 54, 94, 235, 167, 2, 187, 249, 71, 31, 59, 27, 19, 166, 78, 236, 102, 48, + 29, 142, 41, 189, 22, 146, 218, 69, 147, 165, 240, 235, + ]; + glib::Bytes::from_owned(secret) + }; +} + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstsodium::plugin_register_static().unwrap(); + }); +} + +#[test] +fn test_pipeline() { + init(); + + let pipeline = gst::Pipeline::new(Some("sodium-decrypter-test")); + + let input_path = { + let mut r = PathBuf::new(); + r.push(env!("CARGO_MANIFEST_DIR")); + r.push("tests"); + r.push("encrypted_sample"); + r.set_extension("enc"); + r + }; + + let filesrc = gst::ElementFactory::make("filesrc", None).unwrap(); + filesrc + .set_property("location", &input_path.to_str().unwrap()) + .expect("failed to set property"); + + let dec = gst::ElementFactory::make("sodiumdecrypter", None).unwrap(); + dec.set_property("sender-key", &*SENDER_PUBLIC) + .expect("failed to set property"); + dec.set_property("receiver-key", &*RECEIVER_PRIVATE) + .expect("failed to set property"); + + // the typefind element here is cause the decrypter only supports + // operating in pull mode bu the filesink wants push-mode. + let typefind = gst::ElementFactory::make("typefind", None).unwrap(); + let sink = gst::ElementFactory::make("appsink", None).unwrap(); + + pipeline + .add_many(&[&filesrc, &dec, &typefind, &sink]) + .expect("failed to add elements to the pipeline"); + gst::Element::link_many(&[&filesrc, &dec, &typefind, &sink]) + .expect("failed to link the elements"); + + let adapter = Arc::new(Mutex::new(gst_base::UniqueAdapter::new())); + + let sink = sink.downcast::<gst_app::AppSink>().unwrap(); + let adapter_clone = adapter.clone(); + sink.set_callbacks( + gst_app::AppSinkCallbacks::new() + // Add a handler to the "new-sample" signal. + .new_sample(move |appsink| { + // Pull the sample in question out of the appsink's buffer. + let sample = appsink.pull_sample().map_err(|_| gst::FlowError::Eos)?; + let buffer = sample.get_buffer().ok_or(gst::FlowError::Error)?; + + let mut adapter = adapter_clone.lock().unwrap(); + adapter.push(buffer.to_owned()); + + Ok(gst::FlowSuccess::Ok) + }) + .build(), + ); + + pipeline + .set_state(gst::State::Playing) + .expect("Unable to set the pipeline to the `Playing` state"); + + let bus = pipeline.get_bus().unwrap(); + for msg in bus.iter_timed(gst::CLOCK_TIME_NONE) { + use gst::MessageView; + match msg.view() { + MessageView::Error(err) => { + eprintln!( + "Error received from element {:?}: {}", + err.get_src().map(|s| s.get_path_string()), + err.get_error() + ); + eprintln!("Debugging information: {:?}", err.get_debug()); + unreachable!(); + } + MessageView::Eos(..) => break, + _ => (), + } + } + + pipeline + .set_state(gst::State::Null) + .expect("Unable to set the pipeline to the `Playing` state"); + + let expected_output = include_bytes!("sample.mp3"); + + let mut adapter = adapter.lock().unwrap(); + let available = adapter.available(); + assert_eq!(available, expected_output.len()); + let output_buffer = adapter.take_buffer(available).unwrap(); + let output = output_buffer.map_readable().unwrap(); + assert_eq!(expected_output.as_ref(), output.as_ref()); +} + +#[test] +fn test_pull_range() { + init(); + + let pipeline = gst::Pipeline::new(Some("sodium-decrypter-pull-range-test")); + let input_path = { + let mut r = PathBuf::new(); + r.push(env!("CARGO_MANIFEST_DIR")); + r.push("tests"); + r.push("encrypted_sample"); + r.set_extension("enc"); + r + }; + + let filesrc = gst::ElementFactory::make("filesrc", None).unwrap(); + filesrc + .set_property("location", &input_path.to_str().unwrap()) + .expect("failed to set property"); + + let dec = gst::ElementFactory::make("sodiumdecrypter", None).unwrap(); + dec.set_property("sender-key", &*SENDER_PUBLIC) + .expect("failed to set property"); + dec.set_property("receiver-key", &*RECEIVER_PRIVATE) + .expect("failed to set property"); + + pipeline + .add_many(&[&filesrc, &dec]) + .expect("failed to add elements to the pipeline"); + gst::Element::link_many(&[&filesrc, &dec]).expect("failed to link the elements"); + + // Activate in the pad in pull mode + pipeline + .set_state(gst::State::Ready) + .expect("Unable to set the pipeline to the `Playing` state"); + let srcpad = dec.get_static_pad("src").unwrap(); + srcpad.activate_mode(gst::PadMode::Pull, true).unwrap(); + + pipeline + .set_state(gst::State::Playing) + .expect("Unable to set the pipeline to the `Playing` state"); + + // Test that the decryptor is seekable + let mut q = gst::query::Query::new_seeking(gst::Format::Bytes); + srcpad.query(&mut q); + + // get the seeking capabilities + let (seekable, start, stop) = q.get_result(); + assert_eq!(seekable, true); + assert_eq!( + start, + gst::GenericFormattedValue::Bytes(gst::format::Bytes(Some(0))) + ); + assert_eq!( + stop, + gst::GenericFormattedValue::Bytes(gst::format::Bytes(Some(6043))) + ); + + // do pulls + let expected_array_1 = [ + 255, 251, 192, 196, 0, 0, 13, 160, 37, 86, 116, 240, 0, 42, 73, 33, 43, 63, 61, 16, 128, 5, + 53, 37, 220, 28, 225, 35, 16, 243, 140, 220, 4, 192, 2, 64, 14, 3, 144, 203, 67, 208, 244, + 61, 70, 175, 103, 127, 28, 0, + ]; + let buf1 = srcpad.get_range(0, 50).unwrap(); + assert_eq!(buf1.get_size(), 50); + let map1 = buf1.map_readable().unwrap(); + assert_eq!(&map1[..], &expected_array_1[..]); + + let expected_array_2 = [ + 255, 251, 192, 196, 0, 0, 13, 160, 37, 86, 116, 240, 0, 42, 73, 33, 43, 63, 61, 16, 128, 5, + 53, 37, 220, 28, 225, 35, 16, 243, 140, 220, 4, 192, 2, 64, 14, 3, 144, 203, 67, 208, 244, + 61, 70, 175, 103, 127, 28, 0, 0, 0, 0, 12, 60, 60, 60, 122, 0, 0, 0, 12, 195, 195, 195, + 207, 192, 0, 0, 3, 113, 195, 199, 255, 255, 254, 97, 225, 225, 231, 160, 0, 0, 49, 24, 120, + 120, 121, 232, 0, 0, 12, 252, 195, 195, 199, 128, 0, 0, 0, + ]; + let buf2 = srcpad.get_range(0, 100).unwrap(); + assert_eq!(buf2.get_size(), 100); + let map2 = buf2.map_readable().unwrap(); + assert_eq!(&map2[..], &expected_array_2[..]); + + // compare the first 50 bytes of the two slices + // they should match + assert_eq!(&map1[..], &map2[..map1.len()]); + + // request in the middle of a block + let buf = srcpad.get_range(853, 100).unwrap(); + // result size doesn't include the block macs, + assert_eq!(buf.get_size(), 100); + + // read till eos, this also will pull multiple blocks + let buf = srcpad.get_range(853, 42000).unwrap(); + // 6031 (size of file) - 883 (requersted offset) - headers size - (numbler of blcks * block mac) + assert_eq!(buf.get_size(), 5054); + + // read 0 bytes from the start + let buf = srcpad.get_range(0, 0).unwrap(); + assert_eq!(buf.get_size(), 0); + + // read 0 bytes somewhere in the middle + let buf = srcpad.get_range(4242, 0).unwrap(); + assert_eq!(buf.get_size(), 0); + + // read 0 bytes to eos + let res = srcpad.get_range(6003, 0); + assert_eq!(res, Err(gst::FlowError::Eos)); + + // read 100 bytes at eos + let res = srcpad.get_range(6003, 100); + assert_eq!(res, Err(gst::FlowError::Eos)); + + // read 100 bytes way past eos + let res = srcpad.get_range(424_242, 100); + assert_eq!(res, Err(gst::FlowError::Eos)); + + // read 10 bytes at eos -1, should return a single byte + let buf = srcpad.get_range(5906, 10).unwrap(); + assert_eq!(buf.get_size(), 1); + + pipeline + .set_state(gst::State::Null) + .expect("Unable to set the pipeline to the `Playing` state"); +} + +#[test] +fn test_state_changes() { + init(); + + // NullToReady without keys provided + { + let dec = gst::ElementFactory::make("sodiumdecrypter", None).unwrap(); + assert!(dec.change_state(gst::StateChange::NullToReady).is_err()); + + // Set only receiver key + let dec = gst::ElementFactory::make("sodiumdecrypter", None).unwrap(); + dec.set_property("receiver-key", &*RECEIVER_PRIVATE) + .expect("failed to set property"); + assert!(dec.change_state(gst::StateChange::NullToReady).is_err()); + + // Set only sender key + let dec = gst::ElementFactory::make("sodiumdecrypter", None).unwrap(); + dec.set_property("sender-key", &*SENDER_PUBLIC) + .expect("failed to set property"); + assert!(dec.change_state(gst::StateChange::NullToReady).is_err()); + } + + // NullToReady, no nonce provided + { + let dec = gst::ElementFactory::make("sodiumdecrypter", None).unwrap(); + dec.set_property("sender-key", &*SENDER_PUBLIC) + .expect("failed to set property"); + dec.set_property("receiver-key", &*RECEIVER_PRIVATE) + .expect("failed to set property"); + assert!(dec.change_state(gst::StateChange::NullToReady).is_ok()); + } + + // ReadyToNull + { + let dec = gst::ElementFactory::make("sodiumdecrypter", None).unwrap(); + dec.set_property("sender-key", &*SENDER_PUBLIC) + .expect("failed to set property"); + dec.set_property("receiver-key", &*RECEIVER_PRIVATE) + .expect("failed to set property"); + assert!(dec.change_state(gst::StateChange::NullToReady).is_ok()); + } +} diff --git a/generic/sodium/tests/encrypted_sample.enc b/generic/sodium/tests/encrypted_sample.enc Binary files differnew file mode 100644 index 000000000..f63b133a2 --- /dev/null +++ b/generic/sodium/tests/encrypted_sample.enc diff --git a/generic/sodium/tests/encrypter.rs b/generic/sodium/tests/encrypter.rs new file mode 100644 index 000000000..e2f6dc83a --- /dev/null +++ b/generic/sodium/tests/encrypter.rs @@ -0,0 +1,152 @@ +// encrypter.rs +// +// Copyright 2019 Jordan Petridis <jordan@centricular.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// +// SPDX-License-Identifier: MIT + +#[macro_use] +extern crate lazy_static; +extern crate gstsodium; + +use glib::prelude::*; +use gst::prelude::*; + +lazy_static! { + static ref RECEIVER_PUBLIC: glib::Bytes = { + let public = [ + 28, 95, 33, 124, 28, 103, 80, 78, 7, 28, 234, 40, 226, 179, 253, 166, 169, 64, 78, 5, + 57, 92, 151, 179, 221, 89, 68, 70, 44, 225, 219, 19, + ]; + + glib::Bytes::from_owned(public) + }; + static ref SENDER_PRIVATE: glib::Bytes = { + let secret = [ + 154, 227, 90, 239, 206, 184, 202, 234, 176, 161, 14, 91, 218, 98, 142, 13, 145, 223, + 210, 222, 224, 240, 98, 51, 142, 165, 255, 1, 159, 100, 242, 162, + ]; + glib::Bytes::from_owned(secret) + }; + static ref NONCE: glib::Bytes = { + let nonce = [ + 144, 187, 179, 230, 15, 4, 241, 15, 37, 133, 22, 30, 50, 106, 70, 159, 243, 218, 173, + 22, 18, 36, 4, 45, + ]; + glib::Bytes::from_owned(nonce) + }; +} + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstsodium::plugin_register_static().unwrap(); + // set the nonce + std::env::set_var("GST_SODIUM_ENCRYPT_NONCE", hex::encode(&*NONCE)); + }); +} + +#[test] +fn encrypt_file() { + init(); + + let input = include_bytes!("sample.mp3"); + let expected_output = include_bytes!("encrypted_sample.enc"); + + let mut adapter = gst_base::UniqueAdapter::new(); + + let enc = gst::ElementFactory::make("sodiumencrypter", None).unwrap(); + enc.set_property("sender-key", &*SENDER_PRIVATE) + .expect("failed to set property"); + enc.set_property("receiver-key", &*RECEIVER_PUBLIC) + .expect("failed to set property"); + enc.set_property("block-size", &1024u32) + .expect("failed to set property"); + + let mut h = gst_check::Harness::new_with_element(&enc, None, None); + h.add_element_src_pad(&enc.get_static_pad("src").expect("failed to get src pad")); + h.add_element_sink_pad(&enc.get_static_pad("sink").expect("failed to get src pad")); + h.set_src_caps_str("application/x-sodium-encrypted"); + + let buf = gst::Buffer::from_mut_slice(Vec::from(&input[..])); + + assert_eq!(h.push(buf), Ok(gst::FlowSuccess::Ok)); + h.push_event(gst::Event::new_eos().build()); + + println!("Pulling buffer..."); + while let Ok(buf) = h.pull() { + adapter.push(buf); + if adapter.available() >= expected_output.len() { + break; + } + } + + let buf = adapter + .take_buffer(adapter.available()) + .expect("failed to take buffer"); + let map = buf.map_readable().expect("Couldn't map buffer readable"); + assert_eq!(map.as_ref(), expected_output.as_ref()); +} + +#[test] +fn test_state_changes() { + init(); + + // NullToReady without keys provided + { + let enc = gst::ElementFactory::make("sodiumencrypter", None).unwrap(); + assert!(enc.change_state(gst::StateChange::NullToReady).is_err()); + + // Set only receiver key + let enc = gst::ElementFactory::make("sodiumencrypter", None).unwrap(); + enc.set_property("receiver-key", &*RECEIVER_PUBLIC) + .expect("failed to set property"); + assert!(enc.change_state(gst::StateChange::NullToReady).is_err()); + + // Set only sender key + let enc = gst::ElementFactory::make("sodiumencrypter", None).unwrap(); + enc.set_property("sender-key", &*SENDER_PRIVATE) + .expect("failed to set property"); + assert!(enc.change_state(gst::StateChange::NullToReady).is_err()); + } + + // NullToReady + { + let enc = gst::ElementFactory::make("sodiumencrypter", None).unwrap(); + enc.set_property("sender-key", &*SENDER_PRIVATE) + .expect("failed to set property"); + enc.set_property("receiver-key", &*RECEIVER_PUBLIC) + .expect("failed to set property"); + assert!(enc.change_state(gst::StateChange::NullToReady).is_ok()); + } + + // ReadyToNull + { + let enc = gst::ElementFactory::make("sodiumencrypter", None).unwrap(); + enc.set_property("sender-key", &*SENDER_PRIVATE) + .expect("failed to set property"); + enc.set_property("receiver-key", &*RECEIVER_PUBLIC) + .expect("failed to set property"); + assert!(enc.change_state(gst::StateChange::NullToReady).is_ok()); + } +} diff --git a/generic/sodium/tests/sample.mp3 b/generic/sodium/tests/sample.mp3 Binary files differnew file mode 100644 index 000000000..839e90d59 --- /dev/null +++ b/generic/sodium/tests/sample.mp3 |