// Copyright (c) 2017 Emmanuel Gil Peyrot // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. use xso::{AsXml, FromXml}; use crate::data_forms::DataForm; use crate::disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity}; use crate::hashes::{Algo, Hash}; use crate::ns; use crate::presence::PresencePayload; use base64::{engine::general_purpose::STANDARD as Base64, Engine}; use blake2::Blake2bVar; use digest::{Digest, Update, VariableOutput}; use sha1::Sha1; use sha2::{Sha256, Sha512}; use sha3::{Sha3_256, Sha3_512}; /// Represents a capability hash for a given client. /// /// Warning: This protocol is insecure, you may want to switch to /// [ecaps2](../ecaps2/index.html) instead, see [this /// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html). #[derive(FromXml, AsXml, Debug, Clone)] #[xml(namespace = ns::CAPS, name = "c")] pub struct Caps { /// Deprecated list of additional feature bundles. #[xml(attribute(default))] pub ext: Option, /// A URI identifying an XMPP application. #[xml(attribute)] pub node: String, /// The algorithm of the hash of these caps. #[xml(attribute)] pub hash: Algo, /// The hash of that application’s /// [disco#info](../disco/struct.DiscoInfoResult.html). #[xml(attribute(codec = Base64))] pub ver: Vec, } impl PresencePayload for Caps {} impl Caps { /// Create a Caps element from its node and hash. pub fn new>(node: N, hash: Hash) -> Caps { Caps { ext: None, node: node.into(), hash: hash.algo, ver: hash.hash, } } } fn compute_item(field: &str) -> Vec { let mut bytes = field.as_bytes().to_vec(); bytes.push(b'<'); bytes } fn compute_items Vec>(things: &[T], encode: F) -> Vec { let mut string: Vec = vec![]; let mut accumulator: Vec> = vec![]; for thing in things { let bytes = encode(thing); accumulator.push(bytes); } // This works using the expected i;octet collation. accumulator.sort(); for mut bytes in accumulator { string.append(&mut bytes); } string } fn compute_features(features: &[Feature]) -> Vec { compute_items(features, |feature| compute_item(&feature.var)) } fn compute_identities(identities: &[Identity]) -> Vec { compute_items(identities, |identity| { let lang = identity.lang.clone().unwrap_or_default(); let name = identity.name.clone().unwrap_or_default(); let string = format!("{}/{}/{}/{}", identity.category, identity.type_, lang, name); let mut vec = string.as_bytes().to_vec(); vec.push(b'<'); vec }) } fn compute_extensions(extensions: &[DataForm]) -> Vec { compute_items(extensions, |extension| { // TODO: maybe handle the error case? let mut bytes = if let Some(ref form_type) = extension.form_type { form_type.as_bytes().to_vec() } else { vec![] }; bytes.push(b'<'); for field in extension.fields.clone() { if field.var.as_deref() == Some("FORM_TYPE") { continue; } if let Some(var) = &field.var { bytes.append(&mut compute_item(var)); } bytes.append(&mut compute_items(&field.values, |value| { compute_item(value) })); } bytes }) } /// Applies the caps algorithm on the provided disco#info result, to generate /// the hash input. /// /// Warning: This protocol is insecure, you may want to switch to /// [ecaps2](../ecaps2/index.html) instead, see [this /// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html). pub fn compute_disco(disco: &DiscoInfoResult) -> Vec { let identities_string = compute_identities(&disco.identities); let features_string = compute_features(&disco.features); let extensions_string = compute_extensions(&disco.extensions); let mut final_string = vec![]; final_string.extend(identities_string); final_string.extend(features_string); final_string.extend(extensions_string); final_string } /// Hashes the result of [compute_disco()] with one of the supported [hash /// algorithms](../hashes/enum.Algo.html). pub fn hash_caps(data: &[u8], algo: Algo) -> Result { Ok(Hash { hash: match algo { Algo::Sha_1 => { let hash = Sha1::digest(data); hash.to_vec() } Algo::Sha_256 => { let hash = Sha256::digest(data); hash.to_vec() } Algo::Sha_512 => { let hash = Sha512::digest(data); hash.to_vec() } Algo::Sha3_256 => { let hash = Sha3_256::digest(data); hash.to_vec() } Algo::Sha3_512 => { let hash = Sha3_512::digest(data); hash.to_vec() } Algo::Blake2b_256 => { let mut hasher = Blake2bVar::new(32).unwrap(); hasher.update(data); let mut vec = vec![0u8; 32]; hasher.finalize_variable(&mut vec).unwrap(); vec } Algo::Blake2b_512 => { let mut hasher = Blake2bVar::new(64).unwrap(); hasher.update(data); let mut vec = vec![0u8; 64]; hasher.finalize_variable(&mut vec).unwrap(); vec } Algo::Unknown(algo) => return Err(format!("Unknown algorithm: {}.", algo)), }, algo, }) } /// Helper function to create the query for the disco#info corresponding to a /// caps hash. pub fn query_caps(caps: Caps) -> DiscoInfoQuery { DiscoInfoQuery { node: Some(format!("{}#{}", caps.node, Base64.encode(&caps.ver))), } } #[cfg(test)] mod tests { use super::*; use crate::caps; use minidom::Element; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Caps, 48); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Caps, 96); } #[test] fn test_parse() { let elem: Element = "".parse().unwrap(); let caps = Caps::try_from(elem).unwrap(); assert_eq!(caps.node, String::from("coucou")); assert_eq!(caps.hash, Algo::Sha_256); assert_eq!( caps.ver, Base64 .decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=") .unwrap() ); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid_child() { let elem: Element = "K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=".parse().unwrap(); let error = Caps::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in Caps element."); } #[test] fn test_simple() { let elem: Element = "".parse().unwrap(); let disco = DiscoInfoResult::try_from(elem).unwrap(); let caps = caps::compute_disco(&disco); assert_eq!(caps.len(), 50); } #[test] fn test_xep_5_2() { let elem: Element = r#" "# .parse() .unwrap(); let expected = b"client/pc//Exodus 0.9.1 urn:xmpp:dataforms:softwareinfo ipv4 ipv6 Mac 10.5.1 Psi 0.11 "# .parse() .unwrap(); let expected = b"client/pc/el/\xce\xa8 0.11