diff --git a/common/src/crypto.rs b/common/src/crypto.rs index 3670ca4..d0c274c 100644 --- a/common/src/crypto.rs +++ b/common/src/crypto.rs @@ -27,7 +27,7 @@ use chacha20poly1305::aead::{AeadInPlace, NewAead}; use chacha20poly1305::XChaCha20Poly1305; use chacha20poly1305::XNonce; use rand::{CryptoRng, Rng}; -use secrecy::{ExposeSecret, Secret, SecretVec, Zeroize}; +use secrecy::{DebugSecret, ExposeSecret, Secret, SecretVec, Zeroize}; use typenum::Unsigned; #[derive(Debug, thiserror::Error)] @@ -43,7 +43,7 @@ pub enum Error { } // This struct intentionally prevents implement Clone or Copy -#[derive(Default)] +#[derive(Default, PartialEq, Eq)] pub struct Key(chacha20poly1305::Key); impl Key { @@ -55,6 +55,8 @@ impl Key { } } +impl DebugSecret for Key {} + impl AsRef for Key { fn as_ref(&self) -> &chacha20poly1305::Key { &self.0 diff --git a/common/src/lib.rs b/common/src/lib.rs index 5fd9b66..b7080ba 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -22,6 +22,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +use std::convert::Infallible; use std::fmt::Display; use std::str::FromStr; @@ -48,12 +49,28 @@ pub struct ParsedUrl { pub needs_password: bool, } -#[derive(Default)] +#[derive(Default, Debug)] pub struct PartialParsedUrl { pub decryption_key: Option>, pub needs_password: bool, } +#[cfg(test)] +impl PartialEq for PartialParsedUrl { + fn eq(&self, other: &Self) -> bool { + use secrecy::ExposeSecret; + let decryption_key_matches = { + match (self.decryption_key.as_ref(), other.decryption_key.as_ref()) { + (Some(key), Some(other)) => key.expose_secret() == other.expose_secret(), + (None, None) => true, + _ => false, + } + }; + + decryption_key_matches && self.needs_password == other.needs_password + } +} + impl From<&str> for PartialParsedUrl { fn from(fragment: &str) -> Self { // Short circuit if the fragment only contains the key. @@ -100,6 +117,14 @@ impl From<&str> for PartialParsedUrl { } } +impl FromStr for PartialParsedUrl { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(Self::from(s)) + } +} + #[derive(Debug, Error)] pub enum ParseUrlError { #[error("The provided url was bad")] @@ -274,3 +299,92 @@ impl Default for Expiration { Self::UnixTime(Utc::now() + Duration::days(1)) } } + +#[cfg(test)] +mod partial_parsed_url_parsing { + use secrecy::Secret; + + use crate::base64; + use crate::crypto::Key; + use crate::PartialParsedUrl; + + #[test] + fn empty() { + assert_eq!("".parse(), Ok(PartialParsedUrl::default())); + } + + const DECRYPTION_KEY_STRING: &str = "ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE="; + + fn decryption_key() -> Option> { + Key::new_secret(base64::decode(DECRYPTION_KEY_STRING).unwrap()) + } + + #[test] + fn clean_no_password() { + assert_eq!( + DECRYPTION_KEY_STRING.parse(), + Ok(PartialParsedUrl { + decryption_key: decryption_key(), + needs_password: false + }) + ); + } + + #[test] + fn no_password() { + let input = "key:ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE="; + assert_eq!( + input.parse(), + Ok(PartialParsedUrl { + decryption_key: decryption_key(), + needs_password: false + }) + ); + } + + #[test] + fn with_password() { + let input = "key:ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE=!pw"; + assert_eq!( + input.parse(), + Ok(PartialParsedUrl { + decryption_key: decryption_key(), + needs_password: true + }) + ); + } + + #[test] + fn order_does_not_matter() { + let input = "pw!key:ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE="; + assert_eq!( + input.parse(), + Ok(PartialParsedUrl { + decryption_key: decryption_key(), + needs_password: true + }) + ); + } + + #[test] + fn empty_key_pair_gracefully_fails() { + let input = "!!!key:ddLod7sGy_EjFDjWqZoH4i5n_XU8bIpEuEo3-pjfAIE=!!!"; + assert_eq!( + input.parse(), + Ok(PartialParsedUrl { + decryption_key: decryption_key(), + needs_password: false + }) + ); + } + + #[test] + fn invalid_decryption_key_gracefully_fails() { + assert_eq!("invalid key".parse(), Ok(PartialParsedUrl::default())); + } + + #[test] + fn unknown_fields_are_ignored() { + assert_eq!("!!a!!b!!c".parse(), Ok(PartialParsedUrl::default())); + } +}