// 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 crate::data_forms::DataForm; use crate::disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity}; use crate::hashes::{Algo, Hash}; use crate::ns; use crate::presence::PresencePayload; use crate::util::error::Error; use crate::Element; use blake2::VarBlake2b; use digest::{Digest, Update, VariableOutput}; use sha1::Sha1; use sha2::{Sha256, Sha512}; use sha3::{Sha3_256, Sha3_512}; use std::convert::TryFrom; /// Represents a capability hash for a given client. #[derive(Debug, Clone)] pub struct Caps { /// Deprecated list of additional feature bundles. pub ext: Option, /// A URI identifying an XMPP application. pub node: String, /// The hash of that application’s /// [disco#info](../disco/struct.DiscoInfoResult.html). /// /// 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 hash: Hash, } impl PresencePayload for Caps {} impl TryFrom for Caps { type Error = Error; fn try_from(elem: Element) -> Result { check_self!(elem, "c", CAPS, "caps"); check_no_children!(elem, "caps"); check_no_unknown_attributes!(elem, "caps", ["hash", "ver", "ext", "node"]); let ver: String = get_attr!(elem, "ver", Required); let hash = Hash { algo: get_attr!(elem, "hash", Required), hash: base64::decode(&ver)?, }; Ok(Caps { ext: get_attr!(elem, "ext", Option), node: get_attr!(elem, "node", Required), hash, }) } } impl From for Element { fn from(caps: Caps) -> Element { Element::builder("c", ns::CAPS) .attr("ext", caps.ext) .attr("hash", caps.hash.algo) .attr("node", caps.node) .attr("ver", base64::encode(&caps.hash.hash)) .build() } } 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, } } } 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 bytes = string.as_bytes(); let mut vec = Vec::with_capacity(bytes.len()); vec.extend_from_slice(bytes); vec.push(b'<'); vec }) } fn compute_extensions(extensions: &[DataForm]) -> Vec { compute_items(extensions, |extension| { let mut bytes = vec![]; // TODO: maybe handle the error case? if let Some(ref form_type) = extension.form_type { bytes.extend_from_slice(form_type.as_bytes()); } bytes.push(b'<'); for field in extension.fields.clone() { if field.var == "FORM_TYPE" { continue; } bytes.append(&mut compute_item(&field.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 } fn get_hash_vec(hash: &[u8]) -> Vec { hash.to_vec() } /// 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); get_hash_vec(hash.as_slice()) } Algo::Sha_256 => { let hash = Sha256::digest(data); get_hash_vec(hash.as_slice()) } Algo::Sha_512 => { let hash = Sha512::digest(data); get_hash_vec(hash.as_slice()) } Algo::Sha3_256 => { let hash = Sha3_256::digest(data); get_hash_vec(hash.as_slice()) } Algo::Sha3_512 => { let hash = Sha3_512::digest(data); get_hash_vec(hash.as_slice()) } Algo::Blake2b_256 => { let mut hasher = VarBlake2b::new(32).unwrap(); hasher.update(data); let mut vec = Vec::with_capacity(32); hasher.finalize_variable(|slice| vec.extend_from_slice(slice)); vec } Algo::Blake2b_512 => { let mut hasher = VarBlake2b::new(64).unwrap(); hasher.update(data); let mut vec = Vec::with_capacity(64); hasher.finalize_variable(|slice| vec.extend_from_slice(slice)); 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.hash.hash))), } } #[cfg(test)] mod tests { use super::*; use crate::caps; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Caps, 52); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Caps, 104); } #[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, Algo::Sha_256); assert_eq!( caps.hash.hash, 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 { Error::ParseError(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 data = 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